Why does this code cause data race?

2.1k views Asked by At
1 package main
2
3 import "time"
4
5 func main() {
6     m1 := make(map[string]int)
7     m1["hello"] = 1
8     m1["world"] = 2
9     go func() {
10         for i := 0; i < 100000000; i++ {
11             _ = m1["hello"]
12         }
13     }()
14     time.Sleep(100 * time.Millisecond)
15     m2 := make(map[string]int)
16     m2["hello"] = 3
17     m1 = m2
18 }

I run command go run --race with this code and get:

==================
WARNING: DATA RACE
Read at 0x00c420080000 by goroutine 5:
  runtime.mapaccess1_faststr()
      /usr/local/go/src/runtime/hashmap_fast.go:208 +0x0
  main.main.func1()
      /Users/meitu/test/go/map.go:11 +0x80

Previous write at 0x00c420080000 by main goroutine:
  runtime.mapassign()
      /usr/local/go/src/runtime/hashmap.go:485 +0x0
  main.main()
      /Users/meitu/test/go/map.go:16 +0x220

Goroutine 5 (running) created at:
  main.main()
      /Users/meitu/test/go/map.go:13 +0x1aa
==================

m1 and m2 are different variables,why do line 16 and line 11 cause data race?

My go version is 1.8. I guess that is some compile optimization, and somebody can tell me about it? Thank you very much.

2

There are 2 answers

10
icza On

The requirements to have a data race are:

  1. Multiple goroutines accessing the same resource (e.g. a variable) concurrently.
  2. At least one of those accesses is a write.
  3. The accesses are uncynchronized.

In your code all 3 requirements are met:

  1. You have the main goroutine accesssing m1, and the one you start in it also accesses m1. The main goroutine accesses it after the other goroutine has been launched, so they are concurrent.
  2. The main goroutine writes m1 in line #17: m1 = m2.
  3. The accesses are not synchronized, you use no mutex or channels or anything like that (sleeping is not synchronization).

Therefore it's a data race.

The obvious data race is between lines #11 reading m1, and line #17 writing m1.

But! Since line #17 assigns m2 to m1, then when/if the launched goroutine continues to run, it attempts to read m1 which may now be the value of m2 because we assigned m2 to m1. What does this mean? This introduces another data race writing m2 and reading m1.

That is after line #17 if the program does not end immediately (it may, but not necessarily), then the launched goroutine attempts to read from m1 which is now m2 which was last written in line #16, so this explains the "conflict" between lines #11 and #16.

The full go run -race output is as follows:

==================
WARNING: DATA RACE
Write at 0x00c42000e010 by main goroutine:
  main.main()
      /home/icza/gows/src/play/play2.go:17 +0x22f

Previous read at 0x00c42000e010 by goroutine 5:
  [failed to restore the stack]

Goroutine 5 (running) created at:
  main.main()
      /home/icza/gows/src/play/play2.go:9 +0x190
==================
==================
WARNING: DATA RACE
Read at 0x00c42007e000 by goroutine 5:
  runtime.mapaccess1_faststr()
      /usr/local/go/src/runtime/hashmap_fast.go:208 +0x0
  main.main.func1()
      /home/icza/gows/src/play/play2.go:11 +0x7a

Previous write at 0x00c42007e000 by main goroutine:
  runtime.mapassign_faststr()
      /usr/local/go/src/runtime/hashmap_fast.go:598 +0x0
  main.main()
      /home/icza/gows/src/play/play2.go:16 +0x1fc

Goroutine 5 (running) created at:
  main.main()
      /home/icza/gows/src/play/play2.go:9 +0x190
==================
==================
WARNING: DATA RACE
Read at 0x00c420080088 by goroutine 5:
  main.main.func1()
      /home/icza/gows/src/play/play2.go:11 +0x90

Previous write at 0x00c420080088 by main goroutine:
  main.main()
      /home/icza/gows/src/play/play2.go:16 +0x212

Goroutine 5 (running) created at:
  main.main()
      /home/icza/gows/src/play/play2.go:9 +0x190
==================
Found 3 data race(s)
exit status 66
5
Sarath Sadasivan Pillai On

Data race

A data race occurs when two goroutines access the same variable concurrently and at least one of the accesses is a write.

Instructions Reorder

compilers and processors may reorder the reads and writes executed within a single goroutine as far as the reordering does not change the behaviour within the routine, it doesn' ensure behaviour of other goroutines to be unaffected

m2["hello"] = 3
m1 = m2

Can be reordered to

m1 = m2
m2["hello"] = 3

That won't alter the behaviour of the main routine and thus race checking will consider that also for evaluating race condition. Now we have m2["hello"] = 3 causing the race condition and it prints out the same with its original line number