The use of Go when programming highly concurrent applications doesn’t prevent you from writing a system that features race conditions.
These race conditions can cause unexpected issues with your systems that are both hard to debug and at times, even harder to fix.
Thus, we need to be able to write Go programs that can execute concurrently in a safe manner without impacting performance. This is where the mutex comes into play.
As long as the above piece of code is accessed by a single Goroutine, there shouldn't be any problem.
Let's see why this code will fail when there are multiple Goroutines running concurrently. For the sake of simplicity lets assume that we have 2 Goroutines running the above line of code concurrently.
Internally the above line of code will be executed by the system in the following steps(there are more technical details involving registers, how addition works and so on but for the sake of this tutorial lets assume that these are the three steps),
We have assumed the initial value of x to be 0. Goroutine 1 gets the initial value of x, computes x + 1 and before it could assign the computed value to x, the system context switches to Goroutine 2.
Now Goroutine 2 gets the initial value of x which is still 0, computes x + 1. After this the system context switches again to Goroutine 1.
Now Goroutine 1 assigns its computed value 1 to x and hence x becomes 1. Then Goroutine 2 starts execution again and then assigns its computed value, which is again 1 to x and hence x is 1 after both Goroutines execute.
Now lets see a different scenario of what could happen.
In the above scenario, Goroutine 1 starts execution and finishes all its three steps and hence the value of x becomes 1. Then Goroutine 2 starts execution.
Now the value of x is 1 and when Goroutine 2 finishes execution, the value of x is 2.
So from the two cases you can see that the final value of x is 1 or 2 depending on how context switching happens. This type of undesirable situation where the output of the program depends on the sequence of execution of Goroutines is called race condition.
In the above scenario, the race condition could have been avoided if only one Goroutine was allowed to access the critical section of the code at any point of time. This is made possible by using Mutex.
In the program above, we have created a buffered channel of capacity 1 and this is passed to the increment Goroutine in line no. 18. This buffered channel is used to ensure that only one Goroutine access the critical section of code which increments x.
This is done by passing true to the buffered channel in line no. 8 just before x is incremented. Since the buffered channel has a capacity of 1, all other Goroutines trying to write to this channel are blocked until the value is read from this channel after incrementing x in line no. 10. Effectively this allows only one Goroutine to access the critical section.
We have solved the race condition problem using both mutexes and channels. So how do we decide what to use when. The answer lies in the problem you are trying to solve. If the problem you are trying to solve is a better fit for mutexes then go ahead and use mutex. Do not hesitate to use mutex if needed. If the problem seems to be a better fit for channels, then use it :).
Most Go newbies try to solve every concurrency problem using a channel as it is a cool feature of the language. This is wrong. The language gives us the option to either use Mutex or Channel and there is no wrong in choosing either.
In general use channels when Goroutines need to communicate with each other and mutexes when only one Goroutine should access the critical section of code.
In the case of the problem which we solved above, I would prefer to use mutex since this problem does not require any communication between the goroutines. Hence mutex would be a natural fit.
My advice would be to choose the tool for the problem and do not try to fit the problem for the tool :)
This brings us to an end of this tutorial. Have a great day.
So, in this tutorial, we had a look at the joys of race conditions and how they can wreck havoc on an unsuspecting concurrent system.
We then looked at how we could use mutexes in order to shield us from the evil that is race conditions and ensure that our systems work the way we intended regardless of the number of goroutines present within it!
Hopefully, you found this tutorial useful! If you have any comments or feedback, I would love to hear them in the comment section below. Happy Coding!