The stenght of Go language is concurrency support, the ability to spawn multiple goroutines.
As it lets us divide our program into concurrent (“parallel”) processes. But comes with a set of new issues we have to consider, related to shared memory and synchronization.
What Mutex
is.
This spawns 1000 goroutines incrementing the counter variable, then prints the counter value.
Because fmt.Println(counter) may be called before all 1000 goroutines finished.
by using a sync.WaitGroup, Println(counter) will wait for all goroutines to finish:
wg.Wait() will block the main goroutine until all goroutines will call wg.Done().
because of a Race Condition
:
Multiple goroutines are accessing the counter variable simultaneously.
Since counter++ is just equivalent of
some goroutines will read the same value from counter and set the incremented value to be the same.
We need a mechanism that allows goroutines to get a hold of a certain
“flag”,
using an atomic operation
, will ensure that only one goroutine can invoke the operation at a time.
Whoever is holding this “flag”, will block others, until the flag will be released.
Once released (with mutex.Unlock()), the blocked goroutine will be able to acquire the “flag” and proceed.
If we’ll put a Mutex lock in front of the line counter++, we can ensure that only a single goroutine can access the counter variable at a time.
Suppose we have an expensive computation that takes a lot of time and RAM to complete:
Invoking go Expensive() a thousand times, can easily reach the memory limit. Therefore, it’s important to control the number of running goroutines at the same time.
We are spawning 10 goroutines, each takes a second to complete and we are printing the number of currently running goroutines. How many goroutines will run simultaneously at the most?
Without restricting the number of goroutines, we can easily reach a big number of goroutines, reaching resources limit.
A Semaphore
is
very similar to a Mutex, but with a key difference:
While a Mutex has a single “flag”, a Semaphore has a number of slots (usually represented as an integer counter). So if slots is set to 3, the next goroutine that will invoke sm.Acquire(ctx, 1) will be blocked until one of the slots will be available.
In this example we call the sm.Acquire function, outside of the goroutine, right from the main goroutine, which blocks the main goroutine from adding more goroutines.
We can also spawn the goroutine and block that.
In many cases using channels may eliminate the need for Mutex and Semaphore.
This program has a better design:
Channels have a very efficient blocking mechanism. So whenever a goroutine is trying to send to a full channel, it will be blocked until some other goroutine will read the value from the channel.
We’ll write a program that requests a bunch of web pages and sums up the number of occurrences of a word.
But instead of requesting and processing each web page synchronously, We will put every request in its own goroutine, and accumulate the results into our main goroutine.
using one chanel:
Now let’s declare 3 channels:
Let’s spawn each http_get(url) in a separate goroutine, if there’s an error, we’ll send this error to errChan, if not, we’ll send the sum to resChan
Finally, let’s make our main goroutine listen and handle all incoming data from all channels:
This can be achieved, using a select block:
the complete base program: