Concurrency in Go
“Concurrency is about dealing with lots of things at once. It is a way to structure software, particularly as a way to write clean code that interacts well with the real world.” — Rob Pike
Concurrency refers to the design of the system, while parallelism relates to the execution. Concurrent programming is a large topic but it’s also one of the most interesting aspects of the Go language. Much of Golang’s concurrency model is borrowed from the Seminal paper by C.A.R. Hoare called Communicating Sequential Processes, in 1978. The two major things this paper talk about is
- Parallel composition of sequential processes,
- Communication between these processes.
Go is designed with concurrency in mind and allows us to build complex concurrent pipelines. Go’s approach to concurrency can be best phrased by
Do not communicate by sharing memory; instead, share memory by communicating.
In any other mainstream programming language, when concurrent threads need to share data in order to communicate, a lock is applied to the piece of memory. This typically causes all sorts of issues with race conditions, memory management etc. Instead of applying a lock on the shared variable, Go allows you to communicate (or send) the value stored in that variable from one thread to another. The default behavior is that both the thread sending the data and the one receiving the data will wait till the value reaches its destination. The “waiting” of the threads forces proper synchronization between threads when data is being exchanged.
Goroutines & Channels
Goroutines are means of creating concurrent architecture of a program which could possibly execute in parallel in case the hardware allows it. Goroutines are like ampersands ( & ) in shell which allows the tasks to continue in background. A go statement starts the execution of a function call as an independent concurrent thread of control, or Goroutine, within the same address space. If there is a keyword go before a function call, program execution does not wait for the invoked function to complete. Instead, the function begins execution independently in a new goroutine.
Since go keyword allows the goroutine to execute independently, the calling function has to wait till the called function is executed. WaitGroups are the idiomatic way to wait for a collection of goroutines to finish. The main goroutine calls Add to set the number of goroutines to wait for. Then each of the goroutines runs and calls Done when finished.
Channel in Go provides a connection between two goroutines, allowing them to communicate and synchronize the exchange of any resource that is passed through it. It is the channel’s ability to control the goroutines interaction that creates the synchronization mechanism. When a channel is created with no capacity, it is called an unbuffered channel. In turn, a channel created with capacity is called a buffered channel.
Unbuffered channels have no capacity and therefore require both goroutines to be ready to make any exchange. When a goroutine attempts to send a resource to an unbuffered channel and there is no goroutine waiting to receive the resource, the channel will lock the sending goroutine and make it wait. When a goroutine attempts to receive from an unbuffered channel, and there is no goroutine waiting to send a resource, the channel will lock the receiving goroutine and make it wait. Synchronization is inherent in the interaction between the send and the receive. One can not happen without the other.
Buffered channels have a capacity and therefore can behave a bit differently. When a goroutine attempts to send a resource to a buffered channel and the channel is full, the channel will lock the goroutine and make it wait until a buffer becomes available. If there is room in the channel, the send can take place immediately and the goroutine can move on. When a goroutine attempts to receive from a buffered channel and the buffered channel is empty, the channel will lock the goroutine and make it wait until a resource has been sent. If the buffer is full or if there is nothing to receive, a buffered channel will behave very much like an unbuffered channel.
unbufferedchannel := make(chan string)
bufferedchannel := make(chan int, 20)
Channels are first class values in Golang, they can be passed to a function, returned by a function, used as struct elements, or passed through another channel. The optional <- operator specifies the channel direction, send or receive. A channel may be constrained only to send or only to receive by conversion or assignment.
chan T // can be used to send and receive values of type T
chan <- float64 // can only be used to send float64s
<-chan int // can only be used to receive ints
Below is a program that demonstrates how a channel can be used for both sending and receiving by any number of goroutines.
$ go run channels.go
Anna sent a message to Cody.
Jack sent a message to Bob.
Jill sent a message to Dave.
It also shows how the select statement can be used to choose one out of several communications. A select statement chooses which of a set of possible send or receive operations will proceed. It looks similar to a switch statement but with the cases all referring to communication operations.
It is also possible to iterate through your channels using the range function.
Closing the channel is important before ranging over it, else will lead to panics. Once a channel has been closed, you cannot send a value on the channel, but you can still receive from the closed channel.
Other resources on Go concurrency :