Overview

Yes, I do mean race, not static, and Go is popular for its simplicity and great concurrent development experience, but we often write code with Data Race, but Go can often help us check it out, and this article will try to explain how Go does it.

What is Data Race

In concurrent programming, data race may occur when multiple threads, threads or processes read or write to a memory area at the same time. Data race is when two or more threads access the same shared variable at the same time without reasonable synchronization, and at least one thread is writing to the value of the variable. If these accesses are by different threads and at least one of the accesses is a write access, then the situation is called data race.

Data contention can lead to unpredictable program behavior, including crashes, deadlocks, infinite loops, etc. Therefore, it is important to avoid data contention as a problem in concurrent programming. A common solution is to use synchronization mechanisms, such as locks, semaphores, etc., to ensure that access to shared variables by different threads is ordered.

Go detects data contention

Go’s own tool chain provides several tools to detect data contention, namely

When he detects a Data Race, he reports an error in this format:

  1. [root@liqiang.io]# go test -race liqiang.io/liuliqiang/testpackage
  2. WARNING: DATA RACE
  3. Read by goroutine 185:
  4. net.(*pollServer).AddFD()
  5. src/net/fd_unix.go:89 +0x398
  6. net.(*pollServer).WaitWrite()
  7. src/net/fd_unix.go:247 +0x45
  8. net.(*netFD).Write()
  9. src/net/fd_unix.go:540 +0x4d4
  10. net.(*conn).Write()
  11. src/net/net.go:129 +0x101
  12. net.func·060()
  13. src/net/timeout_test.go:603 +0xaf
  14. Previous write by goroutine 184:
  15. net.setWriteDeadline()
  16. src/net/sockopt_posix.go:135 +0xdf
  17. net.setDeadline()
  18. src/net/sockopt_posix.go:144 +0x9c
  19. net.(*conn).SetDeadline()
  20. src/net/net.go:161 +0xe3
  21. net.func·061()
  22. src/net/timeout_test.go:616 +0x3ed
  23. Goroutine 185 (running) created at:
  24. net.func·061()
  25. src/net/timeout_test.go:609 +0x288
  26. Goroutine 184 (running) created at:
  27. net.TestProlongTimeout()
  28. src/net/timeout_test.go:618 +0x298
  29. testing.tRunner()
  30. src/testing/testing.go:301 +0xe8

Although there is a lot of content, there are usually only two keywords you need to look at:

Common solutions

This allows you to analyze why Data Race occurs. Once you have analyzed the cause, the solutions I have used are

How Go is implemented

Once we know what a Data Race is and how it is commonly solved, let’s take a look at how Go detects Data Races. According to the definition of Data Race, we know that in order for a Data Race to occur, there must be two people reading and writing to the same memory at the same time, during which there is a crossover process, so Go actually implements it by this principle.

Go detects Data Race in a way similar to locking, that is, it punches before and after the memory access, for example, in this original code:

  1. [root@liqiang.io]# cat raw.go
  2. func main() {
  3. go func() {
  4. x = 1
  5. }()
  6. fmt.Println(x)
  7. }

When Data Race (-race) is turned on, the Go-generated code may look like:

  1. [root@liqiang.io]# cat compile.go
  2. func main() {
  3. go func() {
  4. // Notice Race Detector there will be a write operation
  5. race.WriteAcquire(&x)
  6. x = 1
  7. // Notice Race Detector the write operation will be finish
  8. race.WriteRelease(&x)
  9. }()
  10. // Notice Race Detector there will be a read operation
  11. race.ReadAcquire(&x)
  12. value := x
  13. // Notice Race Detector the read operation will be finish
  14. race.ReadRelease(&x)
  15. fmt.Println(value)
  16. }

It is equivalent to punting before and after memory operations, and then there is a component dedicated to detecting Data Race: Data Race Detector, which can be used to detect if there are conflicting accesses to the same block of memory, and the overall process is

  1. Race Detector uses a data structure called Shadow Memory to store the metadata of memory accesses. For each memory address, Shadow Memory records the two most recent access operations, including the type of operation (read or write), the goroutine of the operation, and the moment when the operation occurred.
  2. Check for concurrent accesses: When Race Detector detects a memory access operation, it checks the Shadow Memory records associated with that operation. If one of the following conditions is found, then data contention is considered to exist:
    • The current operation is a write operation and occurs concurrently (i.e., there is no happens-before relationship) with one of the two most recent access operations (either read or write), and the two access operations are from different Goroutines.
    • The current operation is a read operation and occurs concurrently with one of the most recent write operations (i.e., there is no happens-before relationship), and the two operations are from different Goroutines. 3.
  3. Report data contention: When data contention is detected, Race Detector generates a detailed report including information about where the data contention occurred, the Goroutine involved, and the stack trace.

Different memory handling

The above mentioned is a general idea, but we know that Go has different types of memory, such as the simplest heap memory and stack memory, which have different access domains and different triggering conditions for Data Race (for example, heap memory will have a higher probability of triggering, while stack memory will have a low probability). The following summarizes the different ways of handling different kinds of memory:

Data Race’s goroutine model

Race Detector is tightly integrated with the Go runtime in order to detect data contention in real time during program execution. At runtime, Race Detector’s various functions are distributed across multiple Goroutines and threads. I have summarized some of the more commonly used components below:

We can expand on point 4 of this, as it also involves the coordination of multiple goroutines:

  1. When a memory access operation occurs for a Goroutine, Race Detector checks if other Goroutines competing with it exist. This is done by analyzing the metadata in Shadow Memory, which contains the access history and synchronization relationships for each memory address. 2.
  2. If data contention is detected, Race Detector collects information about the competing Goroutine, including its ID, stack trace, the memory address where the contention occurred, and the associated source code location. 3.
  3. Race Detector can detect multiple data contention events at the same time. For each event, it generates a separate report. The report will contain detailed information about the competing Goroutine to help developers understand and resolve the problem. 4.
  4. Finally, when program execution ends or when a data contention is detected, Race Detector prints all the reports collected to the standard error output (stderr). Each report is displayed individually and in the order in which it occurred. This allows developers to view and analyze data contention events one by one.

Summary

In this article, I have described the use and rationale for Go’s Data Race detection mechanism. It is important to note that while Go’s Data Race is easy to enable and the Go compiler and Race Detector will detect all data contentions for memory accesses whenever possible, they cannot guarantee 100% accuracy and completeness. Therefore, developers still need to follow good programming practices to ensure that synchronization and merging of programs is done correctly and safely.

And as you can see from the previous introduction, adding Data Race detection will definitely increase memory usage and decrease execution speed, according to the official documentation: memory usage will increase by 5-10 times and execution time will decrease by 2-20 times.

Ref