Programs that we mainstream developers develop nowadays, almost all has some sort of concurrency employed in them. Be it database access, user I/O, file up or download, requesting web servers, rendering some animation, all these and more use cases are accomplished using different techniques of concurrency.
GO is one of the modern languages that is highly accredited for its handling of concurrency. This accredition came from GO’s effective implementation of concurrent API’s and the ease of writing concurrent programs in GO. Although concurrent programming is always tougher than the sequential counterpart, still having the basics grasped, working with concurrency is far more verbose in GO than other well established traditional languages, at least to me it is (having previously worked with JS and C# :D).
In this post I will try to touch on the basics of concurrent programming in GO. Before that lets have a look at some tidbits of concurrency model in GO.
The contents of this post is skimmed and researched from the following resources:
- The book: The GO programming language by Alan A. A. Donovan and Brian W. Kernighan
- Wikiepadia
Different concurrency models in GO
In GO, there are 2 paradigms of concurrency.
1. Communicating Sequential Process (CSP)
In this style, values are passed among the concurrent activities (goroutines) but variables are scoped and used in a single activity
In this post, I will try to touch the basics of this paradigm
2. Shared memory multithreading
This is like the traditional multithreaded style of programming seen in languages like java, C# etc, using thread-safe (in GO’s vocab, concurrency-safe) techniques
I will hopefully try to delve onto this in some future post(s)
Goroutines
When to use which paradigm of concurrency in GO depends entirely on the use-case. But whatever the paradigm is, it all starts with goroutine.
In GO, a goroutine is the equivalent of a thread from other mainstream languages, but it is not exactly a thread. Goroutine is a pseudo-thread kind of stuff. Each activity that runs concurrently in GO is individually a goroutine. And all programs starts with one main goroutine.
Goroutines are lightweight, they have variable stack unlike OS threads.
Goroutines runs on top of the OS thread. One thread may execute one or more goroutines of a program. And if necessary, can create more OS threads and move the goroutines around the threads as necessary. The GO runtime is responsible for managing these movements or schedulings. These schedulings are very similar to OS kernal scheduling, but they arent that heavyweight because GO’s scheduling is not required to do *context switching* as the kernal has to do (more on context switch here.
To create a goroutine, just append a keyword go before a function call.
go some_function()
This churns a new goroutine and executes the called function in that goroutine.
Channels
Channels are the communication channel (no pun intended) among goroutines. A channel can be used to share data among the concerned goroutines without stressing too much about concurrency-safety. Channels are typed, meaning one channel is configured to send/recieve a fixed types values or pointers. Send and recieves to and from a channel are blocking operations.
To declare a channel, simply calling `make` creates a channel of the passed type:
ch := make(chan int)
In the above snippet, this line creates a channel of type int . Send and recieve to and from a channel is done using <- operator. To send values on this channel, the channel variable should be on the left side and the value that is to be sent, on right. To recieve just vice versa the send process.
ch <- 10 // this sends 10 to the channel ch
x := <- ch // this receives a value from channel and assigns it to x
In the above snippet, first line will send 10 to channel. On the next receive call, this 10 will be read from the channel. The second line does that, reads a value (in this case 10) from channel ch and assigns it to x. These sends and recieves will happen spanning on multiple goroutines in real world programs.
An example
Lets illustrate the above two entities in code to get a better idea:
package main
import "fmt"
func sum(s []int, c chan int) {
sum := 0
for _, v := range s {
sum += v
}
c <- sum // send sum to c
}
func main() {
s := []int{7, 2, 8, -9, 4, 0}
c := make(chan int)
go sum(s[:len(s)/2], c)
go sum(s[len(s)/2:], c)
x, y := <-c, <-c // receive from c
fmt.Println(x, y, x+y)
}
The example is taken from the channels section of A tour of GO
In this example the sum function calculates the sum of a slice and sends it over to a channel of int type c
The sum function is called in main function in 2 seperate goroutines, once with first half of the slice, then the next half. Each goroutines calculate and sends the values independantly. In the next line, 2 recieves are performed from the channel. They will get the values sent in the channel in orderly fashion, i.e the first one pushed into the channel will be ejected first, then the next.
x, y := <-c, <-c
In this line, each recieve operation will wait, until it gets its value and then move on.
Although this example is pretty simple and to some extend contrived, this gives a basic idea as to how to use goroutines and pass values between them.
In other languages, this simple scenario would require to use mutex locks and other thread safety mechanisms, or complex (or one or 2 levels of) callbacks (JS :D). But the syntax of GO is rather verbose and natural. In my next posts I will try to delve into waitGroups, which is similar to asyn/await in (JS/C#). Until then…