Introduction to Actors in Go

August 25, 2015

Among the many challenges that multi-threaded programming can present is that of the dreaded race condition. Race conditions occur when a thread modifies state and another thread accesses that state without any synchronization events. Actors avoid race conditions by assigning a single thread to act on behalf of other threads to modify state.

Concurrency Problems

racers

This go program is supposed to concurrently increment a counter. It spins up a thousand threads, each calling the increment function. The program correctly waits for each thread to finish, but without any synchronization event to control access to the counter variable, we get unpredictable behavior:

package main

import (
    "fmt"
    "runtime"
    "sync"
)

var counter = 0
var wg = sync.WaitGroup{}

func increment() {
    counter++
    wg.Done()
}

func main() {
    runtime.GOMAXPROCS(runtime.NumCPU())

    for i := 0; i < 1000; i++ {
        wg.Add(1)
        go increment()
    }

    wg.Wait()
    fmt.Println(counter)
}

Here is the output from multiple runs of the program, and the last run has the race dector flag enabled:

[/tmp]$ go run actor.go
939
[/tmp]$ go run actor.go
945
[/tmp]$ go run --race actor.go
==================
WARNING: DATA RACE
Read by goroutine 6:
  main.increment()
      /tmp/actor.go:13 +0x38

Previous write by goroutine 5:
  main.increment()
      /tmp/actor.go:13 +0x54

Goroutine 6 (running) created at:
  main.main()
      /tmp/actor.go:22 +0x7f

Goroutine 5 (finished) created at:
  main.main()
      /tmp/actor.go:22 +0x7f
==================
1000
Found 1 data race(s)
exit status 66

What is an actor?

zoolander

An actor is a big name for a simple concept: Never allowing multiple threads to access state… Instead delegate a single thread to process requests for state access and modification. Let’s modify our code to use an actor thread to execute our increments:

...
var actions = make(chan func())

func actor() {
    for action := range actions {
        action()
    }
}

func main() {
    runtime.GOMAXPROCS(runtime.NumCPU())
    defer close(actions)
    go actor()

    for i := 0; i < 1000; i++ {
        wg.Add(1)
        go func() { actions <- increment }()
    }
    ...

now we get the desired output

[/tmp]$ go run --race actor.go 
1000

Matrix multiplication using actors

Using actors to multiply matrices is nothing new. Here’s how:

Just to review, to get the i, jth entry of the product, take the ith row of the first matrix and multiply entry by entry with the jth column of the second matrix.

matrix_multiply

The ith row of the result can be deduced by considering the ith row of the first matrix being multiplied with all of the second matrix.

vector_multiply

By using an actor for each of the rows, the ith row can be computed and replaced concurrently and in a threadsafe way. Here’s a gist to demonstrate:

gist.github.com/slcjordan/e05de3ab485321ff6cbd