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
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?
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.
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.
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: