The Go Race Detector

11 Jun 2014

Robert Knight

Overview

Motivation

Motivation

// explicit concurrency using 'go' statement
go func() {
    ...
}

// implicit concurrency via standard library
timer.AfterFunc(5 * time.Seconds, func() {
    ...
})

What is a data race?

A condition where the timing or ordering of two memory accesses in a program
affects whether the outcome is correct or not.

Data Race conditions

Two memory accesses are involved in a data race if they:

value := 0
for i := 0; i < 1000000; i++ {
    go func() {
        value += 1
    }()
}
fmt.Printf("%d\n", value)

What does 'Happens Before' mean?

eg. from the Go reference: "A send on a channel happens before the corresponding receive from that channel completes"

Data Races - A buggy example

A concurrent image processing program

func main() {
    done := make(chan bool)
    paths := os.Args[1:]

    for _, path := range paths {
        go func() {
            fmt.Printf("Processing %s\n", path)
            processImage(path)
            done <- true
        }()
    }

    for processed := 0; processed < len(paths); processed++ {
        <-done
    }
}

Data Races - A buggy example

go run simple.go a.png b.png c.png

Output:

Processing c.png
Processing c.png
Processing c.png

Avoiding Races

Follow Go best practices for communicating

Go memory model

Provides details on what guarantees Go provides about the order in which events
happen when:

Using the Race Detector

The Race Detector was introduced in Go 1.1. Available for 64bit Linux, Mac & Windows OSes.

go test -race net/http
go run -race app.go
go build -race path/to/package

A Simple Example - Race Detector Output

go run -race simple.go
WARNING: DATA RACE
Read by goroutine 3:
  main.func·001()
      /home/robert/projects/talks/golang-race-detector/examples/simple.go:27 +0x99

Previous write by main goroutine:
  main.main()
      /home/robert/projects/talks/golang-race-detector/examples/simple.go:25 +0x153

Goroutine 3 (running) created at:
  main.main()
      /home/robert/projects/talks/golang-race-detector/examples/simple.go:30 +0x1e4

A Simple Example - Explained

path variable is captured by reference. Goroutines only see the value
it has by the time they start executing.

func main() {
    done := make(chan bool)
    paths := os.Args[1:]

    for _, path := range paths {
        go func() {
            fmt.Printf("Processing %s\n", path)
            processImage(path)
            done <- true
        }()
    }

    for processed := 0; processed < len(paths); processed++ {
        <-done
    }
}

A Simple Example - Fixed

func main() {
    done := make(chan bool)
    paths := os.Args[1:]

    for _, path := range paths {
        go func(path string) {
            fmt.Printf("Processing %s\n", path)
            processImage(path)
            done <- true
        }(path)
    }

    for processed := 0; processed < len(paths); processed++ {
        <-done
    }
}

Caveats

How the Race Detector Works

Detecting Data Races

In order to detect data races, we need to monitor:

Several approaches we could take for this.

Approach A - Emulate the machine

Approach B - Rewrite the code

The race detector uses a combination of compile time instrumentation
and a runtime library.

At compile time, insert calls into the runtime library when "interesting events"
happen:

Implementation in cmd/gc/racewalk.c

Runtime Components

Memory Access Ordering - Acquire and Release

Shadow Memory

Every 8-byte word of app memory is associated with N (currently == 4) shadow words.

Each shadow word describes a memory access:

Shadow Memory

Implementation - State Machine

Every time a memory access occurs:

Implementation - Race Detection

If, when comparing the new and existing shadow words, we find two memory accesses where:

Houston, we have a race → Output a report

Summary

Further Reading

Usage

Implementation

Notable Examples

Thank you