Concurrency and Parallelism in Golang
Concurrent tools, Yay! now I can run stuff in parallel!!
And this is not true.
Let me tell you this in simple english language. Suppose you’re jogging and you need to tie your shoe lace. What would you do? You stop and tie your laces and get running again. You’re able to handle both the things. That’s concurrency. Again you are jogging and at the same time you want to listen to music, so you’re jogging and listening music at the same time. This is called parallelism.
“Concurrency is about dealing with lots of things at once. Parallelism is about doing lots of things at once.” — Rob Pike
Still not clear?
Let’s understand something more in terms of technicality.
Concurrency is a property of a program where two or more tasks can be in progress simultaneously. Parallelism is a run-time property where two or more tasks are being executed simultaneously. Through concurrency you want to define a proper structure to your program. Concurrency can use parallelism for getting its job done but remember parallelism is not the ultimate goal of concurrency.
Goroutines
In Go, concurrency is achieved by using Goroutines. Goroutines are functions or methods which can run concurrently with others methods and functions. They are very much similar like threads in Java but light weight and cost of creating them is very low.
Advantages of Goroutines over threads are:
- Goroutines have a faster startup time than threads.
- Goroutines come with built-in primitives to communicate safely between themselves called as channels(We will come to it later).
- Goroutines are extremely cheap when compared to threads. They are only a few kb in stack size and the stack can grow and shrink according to needs of the application whereas in the case of threads the stack size has to be specified and is fixed.
To run a method or function concurrently prefix it with keyword ‘go’.
Some basic examples:
Let’s understand this example. We have a function print which is just printing a string define from line 8 to 10. In the main function, we have called this function concurrently by using go as a prefix. So now we have two goroutines, first our main function and second our print function. Control doesn’t wait to execute the goroutine completely and immediately moves to next line of code just after the gouroutine has been called. In line 13, we made the main goroutine to sleep for 1 second so that go print() has enough time to execute before the main goroutine terminates the reason behind doing this is if the main goroutine terminates then the program will be terminated and no other goroutine will run. Try this program by commenting line 13.
Lets try to understand an example using multiple goroutines.
In the above example we have 3 concurrent goroutines running. Main(), printnumbers() and printletters() function and also we have put different sleep timers so as to understand the functioning of it.
The output of above example is :1 a 2 3 b 4 c 5 d e Printing from main
Let’s understand the output of it.
We have printnumbers goroutine printing a number in every 250 milliseconds and printletters goroutine every 400 milliseconds. When you put all these in sequence it will look like this:
In both the examples, we use time.Sleep for seeing the difference between how goroutine works. That’s a simple hack and not how goroutine actually communicates with each other. Communication between all goroutines is done by channels.
Channels
Channels provide a way for goroutines to communicate with one another and synchronize their execution.
Let’s understand this by example, we used to explain goroutine.
We defined a channel ch on line 13 and on line 14 we call print goroutine passing channel as argument. Our print function receives this channels, prints the “Printing from goroutine” and writes true to the channel. When this write is complete, the main goroutine receives the data from the ch channel, till this time our main goroutine was blocked and once it read data from the channel ch, it is unblocked and then the text “Printing from main” is printed.
This program outputs:
Printing from goroutine
Printing from main
Let’s take a slightly difficult example.
In the above example, we define two channels even and odd. When their respective goroutines are called to print even and odd numbers less than 9, in the infinite for loop, written in main, line number 31 is blocked waiting to read data from even channel and similarly line number 32 is waiting from odd channel. For the first time, even channel sends {2, true} stored in {even, ok1} and {1, true} stored in {odd, ok2}. And this is followed till channel is closed and at that time ok1 and ok2 has false value stored in it and loop breaks at that time.
The program outputs:
Received 2 true 1 true
Received 4 true 3 true
Received 6 true 5 true
Received 8 true 7 true
Buffered Channels:
Buffered channels can be created by passing an additional capacity parameter to the make function which specifies the size of the buffer.
ch := make(chan type, capacity)
Example:
In the program above, in line no. 8 we create a buffered channel with a capacity of 2. Since the channel has a capacity of 2, it is possible to write 2 ints into the channel without being blocked. We write 2 ints to the channel in line no. 9 and 10 and the channel does not block. We read the 2 ints written in line nos. 11 and 12 respectively.
If the writes on channel are more than its capacity, then the writes are not processed till its concurrent reading is done from one of the goroutines, and once that is done, it will write new values to the channel.