Concurrency in Go

I chose GoLang to implement the algorithm that I was working on in my masters. You can find my repo here. After this experience I wanted to write on Go’s concurrency as a newbie gopher.

Concurrency was important to Go designers so they developed it built in. Instead of using threads to achieve concurrency like Java, Go defines goroutines. A goroutine is a lightweight thread which can be created with the Go keyword. There is no need to construct any object. In the example bellow go f("goroutine A") creates a gorouite which does f(“goroutine A”) concurrent to the main flow of the program. Then go f("goroutine A") also does f(“goroutine B”) concurrently. As you see in the stdout they are printing concurrently. This example is a modified version from an example by Go by Example.

func f(from string) {
	for i := 0; i < 3; i++ {
		fmt.Println(from, ":", i)
		time.Sleep(time.Millisecond)
	}
}

func main() {

	f("direct")

	go f("goroutine A")
	go f("goroutine B")

	time.Sleep(time.Second)
	fmt.Println("done")
}

Output:

direct : 0
direct : 1
direct : 2
goroutine B : 0
goroutine A : 0
goroutine A : 1
goroutine B : 1
goroutine A : 2
goroutine B : 2
done

Note that the program flow does not wait for goroutines to finish to terminate. For that reason we have put time.Sleep(time.Second) before the program terminates. Go’s way to wait for goroutines to finish is to create a waitGroup. A waitGroup is basically a shared counter initially equal to 0, once a gorutoine starts it increases the waitGroup, when it is finished it decreases its value. Once the waitGroup becomes 0 again it means all gorouitnes are done. Look at this example.

Goroutines share the same memory space, but Go designers do not promote doing that. The Go way to communicate between goruotines is to send messages instead of writing to a shared memory. This design is based on a model called Communicating sequential processes (CSP). Bellow is one of the main Go designers quote:

Don’t communicate by sharing memory; share memory by communicating.

Rob Pike

Furthermore Go authors leave sharing memory only to professionals and emphasize that most developers should not to use low level libraries. Like here in sync/atmoic documentation authors tell the reader to not use the library unless you know what you are doing.

// Copyright 2011 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

// Package atomic provides low-level atomic memory primitives
// useful for implementing synchronization algorithms.
//
// These functions require great care to be used correctly.
// Except for special, low-level applications, synchronization is better
// done with channels or the facilities of the [sync] package.
// Share memory by communicating;
// don't communicate by sharing memory.

Their main claim is that accessing to shared memory causes concurrency bugs. For example if two processes try to update some register concurrently, one can overwrite the other process’s update without seeing it, but this can be avoided if processes sent messages to each other. Go introduces channels for sending messages. A channel is like a pipe. Sender gorouitne sends some message to the channel and the other gorouitne listening to the channel will receive the message. Look at this example by A Tour of Go. They also introduce Buffered Channels which can contain multiple messages. In fact buffered channels are shared queues. A shared data structure is lock-free if and only if in any execution after sufficient time at least one of the processes terminate. Lock-freedom is a common progress condition in concurrent algorithms. Go channels are not lock-free, which means they can cause locks (no process having progress). You can read go channels source code here and find out more about the authors design goals here. They argue that in practice having a complete lock-free implementation is not efficient.

Till now we only discussed good things about Go’s design but there is one more thing. Using channels prevents inconsistency bugs but it can cause deadlock bugs. Consider gorouine A (technically goroutines do nat have name) listening on a channel that gorotuine B is going to write but goroutine B is listening on another channel that gorotine A is going to write. A & B will be waiting forever. Not only this case but also assume gorotune B dies before writing to the channel and then A cannot progress. This paper has done a study on go concurrency bugs and somehow confirms this claim. So be careful using channels out there! Consider all failure possibilities and reason how your Go program can continue without any locks.

P.S: If you want to learn more MIT 6.824 is a good source.