Go Concurrency - Part One
This is going to be a little series on concurrency and Go. Just to capture some ideas and capabilities with the language, along with some code for solving real world problems. But first
What is Concurrency
There is a load of descriptions of what concurrency is, often they’re incorrectly describing parallelism. You can find loads of information, but a great talk is Rob Pike’s concurrency is not parallelism. But in a nut shell, I really like the definition of concurrency being a property of code and parallelism being a property of execution. So essentially, design your code to be concurrent, but you execute it with the possibility of utilizing parallelism. Assuming you have multiple cores that is.
Concurrency is hard
Yeah… Spoiler alert, it’s harder than it seems. I forget where I saw, heard, read this piece, but essentially, isn’t it odd that Go
has the flagship property of being concurrent from the ground up and it’s really simple and intuitive to use it. Which generally it is. But all the books and tutorials around it are at the back of books and not in that much depth or use very non real-world examples. Here, watch me count a loop concurrently….
goroutines
A goroutine is a very inexpensive way to introduce concurrency into your programs. Essentially, you use the go
keyword to trigger it.
func main() {
go func(){
fmt.Println("hello from a goroutine")
}()
}
If you run this, chances are you won’t see any output. The goroutine executes in its own context and is passed to the scheduler, which determines when it will run. You can use some timer after to make sure that the output is shown. So how do you get this information back between contexts?
Channels
Channels allow for synchronization points between programs.
func main() {
c := make(chan string)
go func(c chan) {
time.Sleep(2 * time.Second) // simulate work
c <- "Hello from goroutine"
}(c)
fmt.Println(<-c)
// prints: Hello from goroutine
}
They are inherently blocking, which means whereas the previous example just exited, the main goroutine will wait to receive the response from the goroutine that is doing some work.
Another example
What do think will happen here?
func main() {
jobs := make(chan int)
results := make(chan int)
go worker(jobs, results)
for i := 0; i < 10; i++ {
jobs <- i
}
close(jobs)
for r := range results {
fmt.Println(r)
}
}
func worker(jobs <-chan int, results chan<- int){
for n := range jobs {
results <- n * 2
}
close(results)
}
Answer: Deadlock
These channels are unbuffered.
In an unbuffered channel writing to the channel will not happen until there is some receiver waiting to receive the data.
So our for loop for adding numbers to the jobs
channel will block and create a deadlock. So, how can you fix this?
Answer: One option is to use buffered channels
Buffered channels work because they are non-blocking and assuming buffered channels work for your situation, you can simply change our channel declaration to
jobs := make(chan int, 10)
results := make(chan int, 10)
Another Answer: Wrap the the jobs
loop in it’s own go
routine, while still using unbuffered channels. This works because the jobs
for loop is running in it’s own context.
go func(){
for i := 0; i <10; i++ {
jobs <- i
}
close(jobs)
}()
Closing channels
You can also close channels to help identify when a go routine won’t receive any more data. A good rule here is
only the sending go routine should close a channel
That’s assuming it’s the only sender.
In the example above, we close the jobs channel
from the sending side, knowing that there is no more data going to be sent along it. The same with the result channel
, we close it at a point where no more records will be sent along it. In practice there are nicer ways to do this, with quit channels and select
statements.
Another thing I missed when I first starting working with go, is that closing a channel does trigger a message to the receivers. Here’s an example
func main() {
jobs := make(chan int)
fmt.Println(<-jobs)
// Deadlock :-(
}
But if we close it:
func main() {
jobs := make(chan int)
close(jobs)
fmt.Println(<-jobs)
// 0
}
This is just a brief article into some basic concurrency, i’ll look to add some more articles