Golang中使用互斥锁时为什么会遇到数据竞争问题

Golang中使用互斥锁时为什么会遇到数据竞争问题

package main

import (
	"fmt"
	"runtime"
	"sync"
)

var wg sync.WaitGroup
func main() {
	fmt.Println("GoRoutine:", runtime.NumGoroutine())
	fmt.Println("NumCPU:", runtime.NumCPU())

	cnt:= 0
	const gs = 100
	wg.Add(gs)
	var mtx sync.Mutex
	for i := 0; i < gs; i++ {
		go func() {
			mtx.Lock()
			v := cnt
			v++
			runtime.Gosched()
			cnt= v
                    mtx.Unlock()
			wg.Done()
		}()
		fmt.Println("GoRoutine:", runtime.NumGoroutine())
	}
	fmt.Println("cnt:", cnt)
	wg.Wait()
}

更多关于Golang中使用互斥锁时为什么会遇到数据竞争问题的实战教程也可以访问 https://www.itying.com/category-94-b0.html

5 回复

非常感谢 @mje ❤️ 🙏

更多关于Golang中使用互斥锁时为什么会遇到数据竞争问题的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


嗨,caster!

谢谢你 ❤️

能解释一下为什么吗?不管怎样,非常感谢 😄

因为在等待所有递增cnt的goroutine完成之前,您就从主goroutine中打印了cnt,所以它会在所有那些goroutine完成之前的某个不确定时间打印出cnt

您本可以在主goroutine中打印cnt时锁定互斥锁,但打印出的cnt值仍然是不确定的。

func main() {
    fmt.Println("hello world")
}

试试这个

package main

import (
	"fmt"
	"runtime"
	"sync"
)

var wg sync.WaitGroup

func main() {
	fmt.Println("GoRoutine:", runtime.NumGoroutine())
	fmt.Println("NumCPU:", runtime.NumCPU())

	cnt := 0
	const gs = 100
	wg.Add(gs)
	var mtx sync.Mutex
	for i := 0; i < gs; i++ {
		go func() {
			mtx.Lock()
			v := cnt
			v++
			runtime.Gosched()
			cnt = v
			mtx.Unlock()
			wg.Done()
		}()
		fmt.Println("GoRoutine:", runtime.NumGoroutine())
	}
	wg.Wait()
	fmt.Println("cnt:", cnt)
}

这是一个典型的数据竞争问题,根源在于互斥锁的保护范围不完整。虽然代码中使用了互斥锁,但锁的释放位置不当,导致部分临界区操作暴露在锁保护之外。

具体问题分析:

  1. mtx.Unlock() 被放在了 wg.Done() 之后,这意味着 cnt = v 操作完成后,锁就被释放了
  2. runtime.Gosched() 在锁内部被调用,这会导致调度器切换到其他goroutine
  3. 由于锁已经释放,其他goroutine可以同时访问 cnt,造成数据竞争

修正后的代码应该将所有对共享变量的读写操作都放在锁的保护范围内

package main

import (
	"fmt"
	"runtime"
	"sync"
)

var wg sync.WaitGroup

func main() {
	fmt.Println("GoRoutine:", runtime.NumGoroutine())
	fmt.Println("NumCPU:", runtime.NumCPU())

	cnt := 0
	const gs = 100
	wg.Add(gs)
	var mtx sync.Mutex
	
	for i := 0; i < gs; i++ {
		go func() {
			mtx.Lock()
			// 所有对cnt的操作都在锁的保护下
			v := cnt
			v++
			// 如果需要主动让出CPU,也应该在锁内部
			runtime.Gosched()
			cnt = v
			mtx.Unlock()  // 先解锁
			wg.Done()     // 后完成等待组
		}()
		fmt.Println("GoRoutine:", runtime.NumGoroutine())
	}
	
	wg.Wait()  // 等待所有goroutine完成
	fmt.Println("cnt:", cnt)  // 这里应该输出100
}

更简洁的写法是使用互斥锁直接保护整个操作:

go func() {
    mtx.Lock()
    cnt++  // 直接递增,无需中间变量
    mtx.Unlock()
    wg.Done()
}()

或者使用 defer 确保锁一定会被释放:

go func() {
    mtx.Lock()
    defer mtx.Unlock()
    cnt++
    wg.Done()
}()

关键原则:互斥锁应该保护从读取共享数据到写入共享数据的完整操作序列,任何中间的操作(包括主动调度)都应该在锁的保护范围内。

回到顶部