Race conditions in Golang

Writing multithreaded programs is an important job that requires planning before writing. If you're using a single-threaded language, such as JavaScript, knowing the basics will do. But if you are familiar with server-side programming languages ​​such as C or C++, their multithreading concepts are similar, and their usage is slightly different. In this blog post, I want to explain how race conditions occur and how to implement synchronous data access using Golang.

What is a race condition?

A race condition occurs when multiple threads try to access and modify the same data (memory address). For example, if one thread tries to increment an integer while another thread tries to read it, this will cause a race condition. On the other hand, if the variable is read-only, there is no race condition. In golang, when using Goroutines, threads are created implicitly.

Let's try to create a race condition. The easiest way is to use multiple goroutines, and at least one of the goroutines must write to the shared variable.

The following code demonstrates a simple way to create a race condition.

  • goroutine reads variable named "sharedInt"
  • Another goroutine writes to the same variable by incrementing its value.
package main

import "time"

// This is an example of race condition
// 2 goroutines tries to read&write sharedInt and there is no access control.

var sharedInt int = 0
var unusedValue int = 0

func runSimpleReader() {
 for {
  var val int = sharedInt
  if val%10 == 0 {
   unusedValue = unusedValue + 1
  }
 }
}

func runSimpleWriter() {
 for {
  sharedInt = sharedInt + 1
 }
}

func startSimpleReadWrite() {
 go runSimpleReader()
 go runSimpleWriter()
 time.Sleep(10 * time.Second)
}

If you run this code, it won't crash, but the read goroutine will access a stale copy of "sharedInt". If you run your code with the built-in race condition checker, the go compiler will raise this issue.

go run -race .

A small note about Golang's race condition checker: if your code occasionally accesses shared variables, it may fail to detect race conditions. To detect it, the code should run under high load and a race condition must occur.

You can see the output of the race condition checker. It prompts that data access is out of sync.

How do we solve this problem? If the shared data is a single variable, we can use sync/atomicthe counter provided in the package. In the example below atomic.LoadInt64()/atomic.AddInt64(), instead of accessing the shared variable directly, we can access it using a pair. The race condition checker will no longer prompt for unsynchronized data access.

package main

import (
 "sync/atomic"
 "time"
)

var sharedIntForAtomic int64 = 0
var unusedValueForAtomic int = 0

func runAtomicReader() {
 for {
  var val int64 = atomic.LoadInt64(&sharedIntForAtomic)
  if val%10 == 0 {
   unusedValueForAtomic = unusedValueForAtomic + 1
  }
 }
}

func runAtomicWriter() {
 for {
  atomic.AddInt64(&sharedIntForAtomic, 1)
 }
}

func startAtomicReadWrite() {
 go runAtomicReader()
 go runAtomicWriter()
 time.Sleep(10 * time.Second)
}

This solves our problem when working with primitive variables, but in many cases we need to access multiple variables and use complex data structures. In these cases, it is easier to solve the problem using a mutex to control access.

以下示例演示了对Map的非同步访问。使用复杂的数据结构时,竞态条件可能会导致崩溃。因此,如果我们在没有启用竞态检查的情况下运行此示例,go 运行时将提示并发访问,并且进程将退出。

fatal error: concurrent map read and map write
package main

import "time"

// This is an example of race condition
// 2 goroutines tries to read&write sharedMap and there is no access control.
// This code should raise a panic condition

var sharedMap map[string]int = map[string]int{}

func runSimpleMapReader() {
 for {
  var _ int = sharedMap["key"]
 }
}

func runSimpleMapWriter() {
 for {
  sharedMap["key"] = sharedMap["key"] + 1
 }
}

func startMapReadWrite() {
 sharedMap["key"] = 0

 go runSimpleMapReader()
 go runSimpleMapWriter()
 time.Sleep(10 * time.Second)
}

可以通过控制对临界区的访问来解决此问题。在这个例子中,临界区是我们读写“sharedMap”的地方。在下面的示例中,我们调用mutex.Lock()mutex.Unlock()对来控制访问。

互斥锁是如何工作的?

  • 互斥锁是在解锁状态下创建的。
  • 当第一次调用 mutex.Lock() 时,互斥锁状态更改为 Locked。
  • 对 mutex.Lock() 的任何其他调用都将阻塞 goroutine,直到调用 mutex.Unlock()
  • 因此,只有一个线程可以访问临界区。

例如,我们可以使用互斥锁来控制对临界区的访问。我添加了一个上下文来在工作 2 秒后取消 goroutine。

package main

import (
 "context"
 "fmt"
 "sync"
 "time"
)

var sharedMapForMutex map[string]int = map[string]int{}
var mapMutex = sync.Mutex{}
var mutexReadCount int64 = 0

func runMapMutexReader(ctx context.Context, readChan chan int) {
 readCount := 0
 for {
  select {
  case <-ctx.Done():
   fmt.Println("reader exiting...")
   readChan <- readCount
   return
  default:
   mapMutex.Lock()
   var _ int = sharedMapForMutex["key"]
   mapMutex.Unlock()
   readCount += 1
  }
 }
}

func runMapMutexWriter(ctx context.Context) {
 for {
  select {
  case <-ctx.Done():
   fmt.Println("writer exiting...")
   return
  default:
   mapMutex.Lock()
   sharedMapForMutex["key"] = sharedMapForMutex["key"] + 1
   mapMutex.Unlock()
   time.Sleep(100 * time.Millisecond)
  }
 }
}

func startMapMutexReadWrite() {
 testContext, cancel := context.WithCancel(context.Background())

 readCh := make(chan int)
 sharedMapForMutex["key"] = 0

 numberOfReaders := 15
 for i := 0; i < numberOfReaders; i++ {
  go runMapMutexReader(testContext, readCh)
 }
 go runMapMutexWriter(testContext)

 time.Sleep(2 * time.Second)

 cancel()

 totalReadCount := 0
 for i := 0; i < numberOfReaders; i++ {
  totalReadCount += <-readCh
 }

 time.Sleep(1 * time.Second)

 var counter int = sharedMapForMutex["key"]
 fmt.Printf("[MUTEX] Write Counter value: %v\n", counter)
 fmt.Printf("[MUTEX] Read Counter value: %v\n", totalReadCount)
}

如果我们运行示例代码,go 运行时将不再提示并发读取和写入问题,因为一次只有一个 goroutine 可以访问临界区。在示例中,我使用了 15 个读取器 goroutine 和一个写入器 goroutine。每 100 毫秒更新一次“sharedMap”。在这种情况下,最好使用 RWMutex(读/写互斥锁)。它类似于互斥锁,但它还有另一种锁定机制,可以让多个读者在安全的情况下访问临界区。当写入很少并且读取更常见时,这可能会表现得更好。

RWMutex 是如何工作的?

  • 简单来说,如果没有写入者,多个读可以同时访问临界区。如果写入者试图访问临界区,则所有读取都会被阻止。当写入很少并且读取很常见时,这会更有效。
  • rwMutex.Lock() 和 rwMutex.Unlock() 的工作方式类似于互斥锁-解锁机制。
  • 如果互斥锁处于解锁状态,rwMutex.RLock() 不会阻止任何读取器。这允许多个读者同时访问临界区。
  • 当 rwMutex.Lock() 被调用时;调用者被阻塞,直到所有读者都调用 rwMutex.RUnlock()。此时,任何对 RLock() 的调用都开始阻塞,直到调用 rwMutex.Unlock()。这可以防止发生任何饥饿。
  • 当 rwMutex.Unlock() 被调用时;RLock() 的所有调用者都被解除阻塞并且可以访问临界区。
package main

import (
 "context"
 "fmt"
 "sync"
 "time"
)

var sharedMapForRWMutex map[string]int = map[string]int{}
var mapRWMutex = sync.RWMutex{}
var rwMutexReadCount int64 = 0

func runMapRWMutexReader(ctx context.Context, readChan chan int) {
 readCount := 0
 for {
  select {
  case <-ctx.Done():
   fmt.Println("reader exiting...")
   readChan <- readCount
   return
  default:
   mapRWMutex.RLock()
   var _ int = sharedMapForRWMutex["key"]
   mapRWMutex.RUnlock()
   readCount += 1
  }
 }
}

func runMapRWMutexWriter(ctx context.Context) {
 for {
  select {
  case <-ctx.Done():
   fmt.Println("writer exiting...")
   return
  default:
   mapRWMutex.Lock()
   sharedMapForRWMutex["key"] = sharedMapForRWMutex["key"] + 1
   mapRWMutex.Unlock()
   time.Sleep(100 * time.Millisecond)
  }
 }
}

func startMapRWMutexReadWrite() {
 testContext, cancel := context.WithCancel(context.Background())

 readCh := make(chan int)
 sharedMapForRWMutex["key"] = 0

 numberOfReaders := 15
 for i := 0; i < numberOfReaders; i++ {
  go runMapRWMutexReader(testContext, readCh)
 }
 go runMapRWMutexWriter(testContext)

 time.Sleep(2 * time.Second)

 cancel()

 totalReadCount := 0
 for i := 0; i < numberOfReaders; i++ {
  totalReadCount += <-readCh
 }

 time.Sleep(1 * time.Second)

 var counter int = sharedMapForRWMutex["key"]
 fmt.Printf("[RW MUTEX] Write Counter value: %v\n", counter)
 fmt.Printf("[RW MUTEX] Read Counter value: %v\n", totalReadCount)
}

Mutex 与 RWMutex 性能对比

我运行了五次示例并比较了平均值。结果,RWMutex 执行的读取操作增加了 14.35%。但请注意,这个例子是在特定场景下进行的,因为有 15 个读取器 goroutine 和一个写入器 goroutine。

总结

在这篇博文中,我试图回顾导致竞争条件的非同步数据访问的基础知识,来进一步讨论如何避免竞态条件的发生问题。根据我的个人经验,我更愿意让每个 goroutine 上下文中使用自己的局部变量,并通过使用通道传播数据。设计通过通道或队列进行通信的单线程组件更容易。如果这种方法并不适用于你的场景,这时互斥锁可以派上用场。

推荐

K8s Pod优雅关闭,没你想象的那么简单!

分享下云原生技术之外的另类话题



原创不易,随手关注或者”在看“,诚挚感谢!

本文分享自微信公众号 - 云原生技术爱好者社区(programmer_java)。
如有侵权,请联系 [email protected] 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。

{{o.name}}
{{m.name}}

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=324223223&siteId=291194637
Recommended