Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Go Concurrency Patterns

Go Concurrency Patterns

A brief introduction to concurrency patterns in Go.

Danielle Brook-Roberge

July 31, 2019
Tweet

More Decks by Danielle Brook-Roberge

Other Decks in Programming

Transcript

  1. Get Go­ing Concurrency in Go is based on three basic

    concepts: goroutines, blocking, and channels. The combination of these provides a versatile base for a variety of patterns. The Go standard library also provides a more conventional Mutex type, but I will not be discussing that here Go Concurrency Patterns 2 / 30
  2. Goroutines! Threads in Go are called "goroutines" to highlight their

    lightweight nature All goroutines run in the same address space. Go provides several methods to make this manageable, but it is still possible to create race conditions with insufficient care. Many operations block a goroutine, which will sleep until the blocking operation completes. Go Concurrency Patterns 3 / 30
  3. func waitThenPrint(period time.Duration, message string) { // This call blocks

    the goroutine time.Sleep(period) fmt.Println(message) } func main() { // These statements create goroutines go waitThenPrint(2*time.Second, "Second") go waitThenPrint(1*time.Second, "First") go waitThenPrint(3*time.Second, "Third") time.Sleep(5 * time.Second) } Go Concurrency Patterns 4 / 30
  4. Channels! We avoid the complications of shared memory by communicating

    between goroutines using channels A channel is a multi­reader, multi­writer thread­safe fixed­length queue Channels are typed and can transmit any Go type (including functions and other channels!). We write the type of a channel as chan T where T is the type it will transmit Go Concurrency Patterns 6 / 30
  5. Unbuffered Channels By default, when we create a channel we

    create a zero­length unbuffered channel. Writes to unbuffered channels will block until another goroutine reads the value Similarly, reads to unbuffered channels will block until another goroutine writes a value Go Concurrency Patterns 7 / 30
  6. func receiver(input chan int) { // This will read from

    the channel repeatedly // until we close the channel for n := range input { fmt.Printf("Received: %d\n", n) } fmt.Println("Exiting receiver") } Go Concurrency Patterns 8 / 30
  7. func main() { unbuffered := make(chan int) go receiver(unbuffered) for

    i := 0; i < 3; i++ { fmt.Printf("Sending: %d\n", i) unbuffered <- i fmt.Printf("Sent: %d\n", i) } close(unbuffered) time.Sleep(3 * time.Second) } Go Concurrency Patterns 9 / 30
  8. Channel Buffering If we create a channel with a buffer,

    we can largely avoid blocking goroutines Each buffered channel has a length; if the number of values in the channel is less than that writes will not block Similarly, reads from buffered channels will only block if the channel is empty. Go Concurrency Patterns 10 / 30
  9. func main() { buffered := make(chan int, 3) for i

    := 0; i < 3; i++ { fmt.Printf("Sending: %d\n", i) buffered <- i fmt.Printf("Sent: %d\n", i) } go receiver(buffered) close(buffered) time.Sleep(3 * time.Second) } Go Concurrency Patterns 11 / 30
  10. Fire and Forget Goroutines If we create a new goroutine

    without communication through shared channels or variables, it will proceed separately from the originating goroutine until it completes. This is used in HTTP handlers similarly to an async Express handler in Node.js Since network interactions and similar asynchronous actions block goroutines, we have a similar pattern without needing to explicitly await Go Concurrency Patterns 13 / 30
  11. func fakeDatabaseCall(query string) int { fmt.Println(query) time.Sleep(500 * time.Millisecond) return

    len(query) } func fakeHTTPCall(url string) int { fmt.Println(url) time.Sleep(800 * time.Millisecond) return 200 } Go Concurrency Patterns 14 / 30
  12. func deleteEntity(id int) { query := fmt.Sprintf("SELECT %d", id) remoteID

    := fakeDatabaseCall(query) url := fmt.Sprintf("http://my-fake.domain/api/%d", remoteID) statusCode := fakeHTTPCall(url) if statusCode != 200 { fmt.Printf("Id %d failed", id) } } Go Concurrency Patterns 15 / 30
  13. var ids = []int{5, 10, 309, 8878} func main() {

    for _, id := range ids { go deleteEntity(id) } time.Sleep(5 * time.Second) } Go Concurrency Patterns 16 / 30
  14. Goroutine Pipelines We can use channels to connect multiple goroutines

    into a pipeline Each goroutine other than the first or last reads from one channel and writes to another. This is a special case of the channel­goroutine model's ability to create arbitrary dataflow graphs. Go Concurrency Patterns 18 / 30
  15. func producer(output chan int) { i := 2 // Never

    stops sending // But when main goroutine ends // The program exits for { output <- i i++ } } Go Concurrency Patterns 19 / 30
  16. func sieveSingle(n int, input chan int, output chan int) {

    for i := range input { if i%n != 0 { output <- i } } } Go Concurrency Patterns 20 / 30
  17. func main() { sieveOutput := make(chan int) go producer(sieveOutput) for

    i := 0; i < 20; i++ { // Read a single value from `sieveOutput` and assign to x x := <-sieveOutput newOutput := make(chan int) go sieveSingle(x, sieveOutput, newOutput) sieveOutput = newOutput fmt.Println(x) } } Go Concurrency Patterns 21 / 30
  18. WaitGroup and parallel iteration We can manage a set of

    goroutine workers with a WaitGroup, which is provided in the Go standard library A WaitGroup provides a thread­safe counter and the ability for a goroutine to wait for it to reach 0 The basic WaitGroup pattern is to increment the counter as we spawn goroutines, decrement in each of those goroutines when they complete, and wait in the original goroutine for the others to complete Go Concurrency Patterns 23 / 30
  19. var wg sync.WaitGroup results := make([]int, 5) for i :=

    0; i < 5; i++ { wg.Add(1) // we can create a goroutine with an IIFE go func(id int) { defer wg.Done() time.Sleep(5 * time.Second) results[id] = id + 4 }(i) } wg.Wait() fmt.Println(results) Go Concurrency Patterns 24 / 30
  20. Worker Pools Channels can be used fairly easily to build

    worker pools This can be useful when there's a need to control the total amount of work a given Go process takes on. One example is when consuming messages from Kafka; when uncontrolled it can easily overwhelm the program with work and starve each job to uselessness Go Concurrency Patterns 26 / 30
  21. func worker(input chan int, output chan int) { for x

    := range input { acc := 0 for x > 0 { acc += x x-- time.Sleep(time.Second) } output <- acc } } Go Concurrency Patterns 27 / 30
  22. func main() { // Create worker pool workQueue := make(chan

    int, 5) resultsQueue := make(chan int) for i := 0; i < 5; i++ { go worker(workQueue, resultsQueue) } // Submit some work for x := 0; x < 10; x++ { workQueue <- x } Go Concurrency Patterns 28 / 30
  23. // Read out the work for x := 0; x

    < 10; x++ { result := <-resultsQueue fmt.Println(result) } // Shut down the workers close(workQueue) } Go Concurrency Patterns 29 / 30