Go Concurrency- A primer

manoj s k
Codezillas
8 min readMay 15, 2018

--

World around us is concurrent. At a given time, lot of things happen independent of each other. To solve real world problems, then, we need a way to model this concurrency in a program. Go, as a modern language, comes with concurrency support built along the language making us to avoid depending on third part libraries which are difficult to understand, port and maintain. In this article, I will delve into Go concurrency primitives and use them to design concurrent programs.

Concurrency or Parallelism ?

Definitions first:

  • Concurrency: is a way of composing independent tasks.
  • Parallelism: is actually running multiple independent tasks at the same time.

Say we have a single processor which is used to run our program. We design a way to split our single program into 2 subtasks which are independent of each other. This way of organizing is concurrent design.This code when run on the said machine is not parallely running because we have a single processor and each subtask must share the processor with other and at a time only one task occupies processor.

But say now we have a multi-processor system and we run the given program on that. Since tasks are independent and if we can pipeline them to different processors, we have a parallel computation happening. We converted a concurrent program to a parallel one just by adding another processor(not really, but kind of).

Thus, concurrency is a composition method while parallelism is actual execution of independent tasks at once. Concurrency is the ability to run independently.

Go concurrency primitives

Now lets look at concurrency specific to Golang.

Goroutines

Goroutine is an independently executing function. It is launched by go statement and has its own call stack which can grow or shrink dynamically and run time takes care of handling it. If we have a function foo to execute as independent goroutine, we just need to do go foo() and voila! we have a goroutine created. Now foo() runs in its own stack and the called function can proceed without blocking for it to return.

Go concurrency is composition of independently executing goroutines.

Goroutine is NOT a thread. Goroutines are multiplexed onto threads dynamically.Threads are created on the fly such that no goroutine ever blocks. Go runtime has an abstraction over OS threads with concepts like logical processors. OS scheduler schedules threads to run on physical processors while Go runtime scheduler runs goroutines against logical processors. Each logical processor is associated with one OS thread. Go default is to have a logical processor for every physical processor on the machine. These are used to run goroutines that are created.Even if we have a single processor, huge number of coroutines can be run concurrently.

The go runtime scheduler has a global run queue. As every goroutine is created it is put in this queue. Then they are assigned to a logical processor and put into the logical processors run queue. And then they wait for their turn to run on logical processor. When a processor is running a goroutine that makes a blocking call or explicitly calls runtime.Gosched(), it will be detached from the processor and next one in the queue is run. Once the blocking call returns, the goroutine is again placed to a local run queue.

For threads blocked on network IO calls, they are put in integrated network poller which will indicate when read or write operation is ready and the goroutine moves to a logical processor’s run queue.

A program can have unto 10000 operating system threads by default(can tinker the number in debug.SetMaxThreads function).If it exceeds this limit, program crashes. This is for OS threads, not goroutines. We can shoot goroutines on the fly but a new OS thread is created only when a goroutine is ready to run but all the existing threads are blocked in system calls or threads are locked to goroutines with runtime.LockOSThread calls.

Channels

Just having many independent functions running and printing to console is not very useful. There should be a way for them to communicate with each other. In languages like Java, C++ the concurrency libraries are modeled around communicating between threads by sharing resource. This needed locking ,mutexes etc which were difficult to get right.

Go shares information between resources by communicating. And the primitive we have for communication is channel. Channels are conduits through which data is passed using channel operator(<-). Channels can be used as shown in the block below:

By default, reads and writes to channel block. i.e., if we try to read from a channel, we block till there is some value to read in it. Likewise write blocks till the receiver is ready.(Though we can overcome this by having buffered channels).Thus, channels provide a way to both communicate and synchronize.

Not that we don’t have ways to synchronize access to a global data. There are good old primitives like mutexes, atomic functions etc but Go has better way to compose concurrent functions around channels.

Go way is to share by communicating instead of communicating by sharing as in C++ or Java.

Let’s see the same using an example program

Output is:

Though the goroutine runs forever if it is let , main function’s for loop runs only for 3 iterations. If the channel ch on which the statement inside for loop is not yet written to, the line is blocked. Meanwhile, goroutine foo will run independently writing to channel ch.

  • As soon as goroutine writes j to ch,the main’s for loop’s statement will unblock and execute after reading(kind of popping ) from the channel.
  • After sleeping for a millisecond, goroutine’s ch <-j will be blocked till channel’s value is read in above step after which it unblocks and writes again to channel and then goes to sleep.
  • This cycle repeats for 3 iterations and after which main exits the for loop and prints the Exiting main message and exits. This also results in goroutine exiting.

Though it is a very trivial example, it shows two basic concurrency primitives , goroutines and channels, in action and how channels help in communication and synchronization.

select : a case for channels

For those who come from C, select is similar to switch statement. But instead of integer values like in switch, select lets you wait on multiple channel operations. A select blocks until one of its cases can run.It then executes that particular case. If more than one are ready, it chooses one randomly. Once some channel is available to read and the execution completes, select will exit.

Output to above program is :

Each goroutine writes to appropriate channel at every 2 and 3 seconds respectively and then blocks. In main, select is run in a for loop for 5 iterations.

  • At every iteration, select will execute the case that is ready and then goes back to waiting on them both.
  • Since we are timing such that only one channel has a value during a select run, we can see values from each channel printed alternatively.
  • After 5 runs, select exits and main returns.

select along with goroutines and channels can be used as a powerful tool for organizing code in a concurrent fashion with synchronization and communication acting as tools of concurrency instead of global shared resource and locks.

Mutexes and atomic operations

If there is a need to protect some shared value between different goroutines, we can still use mutexes present in sync package. It is better to club mutex and the value to be protected as a single type and using lock and unlock to access the protected value. A very crude example is shown below:

There are also atomic operations in sync package which guarantee atomic execution. Like AddInt32(), CompareAndSwapInt64() etc there are many primitive functions that provide atomic operations for native types. But it is advised to use other sync primitives and channels for synchronization than using atomics operations.

Race conditions

When multiple goroutines are having access to non synchronized shared resource and they simultaneously try to modify the value of the resource, we get what is called race conditions. We saw above how to do operations atomically using Mutexes or construct code using channels. If we have not used them (?), we can still detect race conditions in the code using race detector flag while building. Yes, wow indeed !

Yes it is that simple. And when we run the executable, clearly RACE warnings will be printed to console and we can check back the code to correct the issues.

Conclusion

While goroutines provide a very light weight threads maintained by Go runtime, channels provide a way to communicate between those goroutines in a synchronized way. Using the tools discussed above, we can model powerful concurrent solutions and ensure ease of understanding and maintenance of the code. Along with these CSP style primitives, we have mutexes, atomic functions and WaitGroups which provide good old, C style concurrency patterns. These features being built into the language along with powerful race detector, makes Golang useful in creating highly concurrent and efficient pieces of software without being bogged down by third party libraries.

--

--

manoj s k
Codezillas

Programmer, Multi media streaming. Traveller and Dreamer