Go race detector
Mar 23, 2017 · 3 minute read · Commentsgolang
Recently, we developed a web application which involved the use of multiple goroutines. As a result data races were just around the corner. Fortunately, Go comes with go race detector
— an incredible tool which makes them easier to detect.
Let’s look at a simplified example:
var servedDevices []Device
devicesToProcess := make(chan []Device, 1)
go func(devicesToProcess chan[]Device) {
select {
case devices := <- devicesToProcess:
for _, device := range devices {
go func(device Device) {
// Operations on Device
servedDevices = append(servedDevices, device)
}(device)
}
}
}(devicesToProcess)
A channel for slices of Device
s is passed as an argument to a goroutine. It reads from the channel and every time a slice comes in, it in turn kicks of a separate goroutine for each Device
to perform some work with that device. When finished, it appends the Device
to a slice defined in the main goroutine.
The program seems to work as expected but using race flag many data races occurred on writing to servedDevices
. The tool provides a essential information about the events:
go run -race main.go
WARNING: DATA RACE
Read at 0x000000568650 by goroutine 8:
main.main.func1.1()
main.go:23 +0x41
Previous write at 0x000000568650 by goroutine 7:
main.main.func1.1()
main.go:23 +0xdf
Goroutine 8 (running) created at:
main.main.func1()
main.go:26 +0x101
Goroutine 7 (finished) created at:
main.main.func1()
main.go:26 +0x101
==================
==================
WARNING: DATA RACE
Read at 0x00c42007a190 by goroutine 8:
runtime.growslice()
slice.go:71 +0x0
main.main.func1.1()
main.go:23 +0x197
Previous write at 0x00c42007a190 by goroutine 7:
main.main.func1.1()
main.go:23 +0xa1
Goroutine 8 (running) created at:
main.main.func1()
main.go:26 +0x101
Goroutine 7 (finished) created at:
main.main.func1()
main.go:26 +0x101
==================
Found 2 data race(s)
exit status 66
Some goroutine conflicts are detected on appending a new Device
to a slice. append
is an atomic operation but it is not thread safe.: when the slice is modified by multiple goroutines, the result is not deterministic. Effect of one goroutine might be reflected, or the other or both. In order to make the list thread safe we can use both a mutex or another channel which will be read in the main goroutine and append to the list in one place. Let’s try the mutex:
type ServedDevices struct {
devices []Device
mutex sync.Mutex
}
func (s *ServedDevices) Add (device Device){
s.mutex.Lock()
defer s.mutex.Unlock()
s.devices = append(s.devices, device)
}
ServedDevices
is thread save because it wraps thread unsafe operation (slice append) by a mutex which serializes access to the slice. ServedDevices
could be used as type for servedDevices
variable and Add
function in instead of raw append
.
As written in the documentation, data races are detected only in running code. For that reason, testing must be as realistic as possible. Moreover, CPU cost and Memory usage could be up to ten times greater than normal use.
Anyway, its flexibility and completeness makes it one of our essential tool for testing applications.
We're looking for developers to help us save energy
If you're interested in what we do and you would like to help us save energy, drop us a line at jobs@enectiva.cz.