programming

Knowing Deadlock and How to Overcome It in Golang

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
comments powered by Disqus