Concurrency in Go

Paul Nechifor

Outline

  • description of Go
  • concurrency approach
  • concurrency language features
  • concurrency patterns
  • conclusion and summary

About Go

  • released in 2009, version 1.0 in 2012
  • a "server software" language
  • is compiled (not interpreted, not JITted) for multiple architectures and OSes
  • only garbage collected
  • has lambdas, closures

Syntax

  • similar to C, but cleaner
  • less verbosity (fewer parenthesis and mostly no semicolons)
  • uses := for assignment and type inference
  • only looping construct is for (includes range loops)
  • generally safer constructs (e.g. no break in switch)

Concurrency Approach

  • concurrency: computations executing in overlapping time periods
  • parallelism: use of multiple execution units
  • coroutines: generalization of subroutines with multiple points for suspending and resuming execution
  • Go uses both for concurrency.
  • Inspired by Hoare's Communicating Sequential Processes (CSP).
  • "Erlang takes some syntax from the CSP model, but with different semantics."
  • "Go takes the semantics but not the syntax."

Goroutines

  • light-weight processes (not coroutines)
  • multiplexed over native threads
  • has own call stack (heap allocated, fragmented, extensible)
  • 100000s are practical

Comparison

  • Unlike Node.js, Python*, and Ruby*, Go is fully parallel.

*Have native threads but use a global interpreter lock.

package main
import (
  "fmt"
  "time"
)
func hello(s string) {
  for {
    fmt.Println("hello", s)
    time.Sleep(time.Second / 10)
  }
}
func main() {
  go hello("world")
  time.Sleep(time.Second)
}
  • Prints "hello world" about 10 times.
  • Program exits when main finishes.
  • This is unlike Java (where all non-daemon threads must end for the program to end), or JavaScript (where the implicit event loop must be empty).

Channels

  • In the original CSP (as in Erlang) processes communicate by name.
  • In Go, goroutines communicate through unnamed channels.
  • Sending and receiving from channels are blocking operations (for the goroutines).
  • This ties together communication and synchronization in a natural way.
  • Channels can be closed, but it is often unnecessary to do do so.

Basic channel communication:

func other(c chan string) {
  c <- "Hello."
}
func main() {
  c := make(chan string)
  go other(c)
  value := <-c
  fmt.Println(value)
}
func main() {
  c := make(chan string)
  c <- "Hello." // Deadlock here.
  value := <-c
  fmt.Println(value)
}
  • If the same code is written in a single function, Go detects the deadlock at runtime.
throw: all goroutines are asleep - deadlock!

Buffered Channels

  • Channels are unbuffered/blocking/synchronous by default.
  • Buffered channels are created by specifying the number of elements to hold before blocking in make.
c := make(chan string, 10)

Daisy Chaining Goroutines with Channels

func pass(from, to chan int) {
  to <- 1 + <-from
}
func main() {
  length := 1000000
  var start = make(chan int)
  var a = start
  var b chan int
  for i := 0; i < length; i++ {
    b = make(chan int)
    go pass(a, b)
    a = b
  }
  start <- 0
  fmt.Println(<-b)
}
  • 1000000 started goroutines (+1 for main)
  • 1000001 opened channels
  • on my laptop it take 2 seconds
  • goroutines are cheap!
time ./daisy-chain
1000000
real	0m2.225s
user	0m1.232s
sys	0m0.980s

Multiple Receivers

  • It's trivial to implement a thread pool pattern.
func worker(id int, c chan string) {
  for {
    fmt.Println("worker", id, <-c)
  }
}
func main() {
  c := make(chan string)
  go worker(0, c)
  go worker(1, c)
  for i := 0; i < 10; i++ {
    c <- fmt.Sprintf("message %d", i)
  }
}
worker 0 message 0
worker 0 message 2
worker 1 message 1
worker 0 message 3
worker 0 message 5
worker 1 message 4
worker 0 message 6
worker 1 message 7
worker 0 message 8

Select

  • Similar to switch statement, except cases refer to receive or send operations.
  • Execution blocks until one operation can be performed.
select {
case v := <-c1:
  fmt.Println("received:", v);
case c2 <- "hello":
  fmt.Println("sent hello to c2");
case c3 <- 55:
  fmt.Println("sent 55 to c3");
}
  • If a default case exists, it is executed and the select is not blocked.

Example pseudorandom send:

func send(c chan int) {
  for {
    select {
      case c <- 0:
      case c <- 1:
    }
  }
}
func main() {
  c := make(chan int)
  go send(c)
  for i := 0; i < 40; i++ {
    fmt.Printf("%d", <-c)
  }
}
  • When there are multiple possible operations, one is chosen pseudorandomly.
1110100000001011011000010100101000101110

Patterns

  • generator
  • quit channel
  • error handling

Generator

  • Generators usually are simpler coroutines.
  • In Go, the generator pattern can be used to produce computationally expensive results.
  • The pattern function makes a channel, launches a goroutine with the channel as a closure and returns the channel.
func gen(total int) chan string {
  c := make(chan string)
  go func() {
    for i := 0; i < total; i++ {
      c <- fmt.Sprintf("result %d", i)
    }
    close(c)
  }()
  return c
}
func main() {
  c := gen(10)
  for {
    if r, ok := <-c; ok {
      fmt.Println(r)
    } else {
      return
    }
  }
}

Simplified by using the range clause for channels.

func gen(total int) chan string {
  c := make(chan string)
  go func() {
    for i := 0; i < total; i++ {
      c <- fmt.Sprintf("result %d", i)
    }
    close(c)
  }()
  return c
}
func main() {
  for r := range gen(10) {
    fmt.Println(r)
  }
}

Quit Channel

func fn(msg string) (out chan string, quit chan bool) {
  out, quit = make(chan string), make(chan bool)
  go func() {
    for i := 0; ; i++ {
      time.Sleep(time.Duration(rand.Intn(1000)) * time.Millisecond)
      select {
      case out <- fmt.Sprintf("%s %d", msg, i):
      case <-quit: return
      }
    }
  }()
  return
}
func main() {
  result, quit := fn("result")
  for {
    fmt.Println(<-result)
    if rand.Intn(2) == 0 {
      quit <- true
      break
    }
  }
}

Conclusions

Advantages:

  • Go's model is simpler than dealing with threads, locks, semaphores, and the rest
  • more intuitive than dealing with callbacks as in Node.js
  • memory allocation can be faster since structures can be embedded in a single zone (one allocation call) unlike Java and others
  • can use parallelism effectively
  • quick compilation, fast startup

Disadvantages:

  • lack of exceptions can make it more verbose and repetitive
  • lack of generics makes it less type-safe
  • lack of JITting can make it slower than Java

References

The End

Questions?