Introduction
One of the problems that occurs when using concurrent
or parallel
is the deadlock
system. What is deadlock? Deadlock is an event where a concurrent process or goroutine waits for each other (lock) so that none of the goroutines can run. So be careful if you create an application or program that implements mutex lock and unlock using goroutines. Well we will try directly how to simulate the golang program when there is a deadlock
.
Mutex Lock And Lock Creation
First we try to create an struct
to hold the count data by using struct and some lock
and unlock
functions. And don’t forget we will also simulate this by using Mutex
. Here is the code that you should create first below.
type UserCount struct {
Name string
TotalCount int
sync.Mutex
}
func (c *UserCount) Lock() {
c.Mutex.Lock()
}
func (c *UserCount) Unlock() {
c.Mutex.Unlock()
}
func (c *UserCount) Change(amount int) {
c.TotalCount = c.TotalCount + amount
}
Creating a TransferPoint Function
In this TransferPoint
function we will simulate that one User
will transfer to another User
so that later we also make it so that there is a deadlock
.
func TransferPoint(user1 *UserCount, user2 *UserCount, amount int) {
user1.Lock()
fmt.Println("Lock processing for user: ", user1.Name)
user1.Change(-amount)
time.Sleep(1 * time.Second)
user2.Lock()
fmt.Println("Lock processing for user: ", user2.Name)
user2.Change(amount)
time.Sleep(1 * time.Second)
user1.Unlock()
user2.Unlock()
}
Next we will create a main()
function that will run a more complete simulation as below.
func main() {
user1 := UserCount{
Name: "Ihsan",
TotalCount: 100,
}
user2 := UserCount{
Name: "Arif",
TotalCount: 100,
}
// call TransferPoint
fmt.Printf("User: %s count: %d\n", user1.Name, user1.TotalCount)
fmt.Printf("User: %s count: %d\n", user2.Name, user2.TotalCount)
}
Then, run the main process so that it will output as below.
User: Ihsan count: 100
User: Arif count: 100
First Try Calling without Goroutine
In experiment 1, we will simulate calling the TransferPoint
function without Goroutine it should be safe and there will be no deadlock
. Why is that? Because the TransferPoint
function is not called at the same time. Well, you try adding or calling the TransferPoint
function after user initialization like this.
TransferPoint(&user1, &user2, 10)
TransferPoint(&user2, &user1, 5)
Then, the result will be like this
Lock processing for user : Ihsan
Lock processing for user : Arif
Lock processing for user : Arif
Lock processing for user : Ihsan
User: Ihsan count: 95
User: Arif count: 105
User Ihsan and Arif have the same point of 100, then they transfer points to each other as follows:
- Ihsan will transfer point 10 to user Arif
- Arif will transfer 5 points to user Ihsan. Then, the calculation of the total count of each of them will be like this:
- Ihsan = 100 - 10 (sent) + 5 (received) = 95
- Arif = 100 - 5 (sent) + 10 (received) = 105
The result of this calculation is true
Experiment Two Calling using Goroutine
In experiment 2, we try to simulate again that there will be deadlock
when the TransferPoint
function call is run using goroutine. Well, you try adding the most of the function with go
and also add time.Sleep
so that the running goroutine process will wait for 3 seconds, then see the code below.
go TransferPoint(&user1, &user2, 10)
go TransferPoint(&user2, &user1, 5)
time.Sleep(3 * time.Second)
Then what will be the result of the program when it is run?
Lock processing for user: Ihsan
Lock processing for user : Arif
User: Ihsan count: 90
User: Arif count: 95
The calculation of the function becomes wrong and why this happens is because of deadlock
. Let’s try to analyze why the calculation is different.
- When User Ihsan makes a transfer then a lock occurs against user Ihsan.
- At the same time User Arif will also make a transfer, so there is a lock on User Arif.
- When User Ihsan will continue the process of adding the amount to Arif, it turns out that user Arif is still
lock
so the process is waiting. - Vice versa, User Arif will also continue the process of adding the amount to Ihsan but it turns out that user Ihsan is still
lock
so he is waiting too. - So, this is where the
deadlock
occurs where the two processes wait for each other until an unspecified time. - In this program we specified a time of 3 seconds, then the process stops and the calculation will be wrong or messy.
Deadlock
Solution
After we know how the deadlock
process occurs then, how is the solution so that it does not happen like that is by doing Unlock
every time there is a change in data from the user. Then, we will change the TransferPoint function as below.
func TransferPoint(user1 *UserCount, user2 *UserCount, amount int) {
user1.Lock()
fmt.Println("Lock processing for user: ", user1.Name)
user1.Change(-amount)
user1.Unlock()
time.Sleep(1 * time.Second)
user2.Lock()
fmt.Println("Lock processing for user : ", user2.Name)
user2.Change(amount)
user2.Unlock()
time.Sleep(1 * time.Second)
}
When run the program will produce the same output as experiment 1.
Lock processing for user : Arif
Lock processing for user : Ihsan
Lock processing for user : Arif
Lock processing for user : Ihsan
User: Ihsan count: 95
User: Arif count: 105