Go语言sync.Mutex锁未按预期工作,打印Map对象异常求助
sync.Mutex看似未按预期工作的原因分析
我不确定为什么sync.Mutex未按预期工作,希望能得到相关建议。以下是我的代码:
package main import ( "fmt" "sync" "time" ) type Container struct { mu sync.Mutex counters map[string]int } func (c *Container) inc(name string) { c.mu.Lock() defer c.mu.Unlock() c.counters[name]++ // fmt.Println("in", name, c.counters) // 这个打印会改变goroutine间的执行表现 time.Sleep(time.Second) } func main() { c := Container{ counters: map[string]int{"a": 0, "b": 0}, } var wg sync.WaitGroup doIncrement := func(name string, n int) { for i := 0; i < n; i++ { c.inc(name) fmt.Println(name, c.counters) } wg.Done() } wg.Add(3) go doIncrement("a", 2) go doIncrement("b", 2) go doIncrement("a", 2) wg.Wait() fmt.Println(c.counters) }
运行这段代码后,我得到了异常输出:
a map[a:2 b:0] a map[a:2 b:0] b map[a:2 b:1] a map[a:4 b:1] a map[a:4 b:1] b map[a:4 b:2] map[a:4 b:2]
我期望能看到a递增到1、2、3、4的日志,但实际并非如此。当我取消inc函数中的打印语句注释后,就能看到符合预期的日志:
in a map[a:1 b:0] a map[a:1 b:0] in a map[a:2 b:0] a map[a:2 b:0] in b map[a:2 b:1] b map[a:2 b:1] in a map[a:3 b:1] a map[a:3 b:1] in a map[a:4 b:1] a map[a:4 b:1] in b map[a:4 b:2] b map[a:4 b:2] map[a:4 b:2]
问题原因与解决方案
问题核心是你在锁的保护范围外读取了共享资源counters:
inc函数里的sync.Mutex只保护了counters的写入操作(c.counters[name]++),但main中fmt.Println(name, c.counters)是无锁的并发读取,不属于临界区操作。- 当一个goroutine执行完
inc解锁后,另一个goroutine可能立刻抢占锁完成下一次递增,而第一个goroutine此时才执行打印,自然会看到已经被更新后的值。 - 打开
inc内的打印后,打印操作处于锁的保护范围内,输出的是当前临界区的真实状态;同时打印本身的IO阻塞会让goroutine的执行顺序更“同步”,但这只是偶然现象,不是正确的并发处理方式。
正确做法
要保证共享资源的并发安全,所有读写操作都必须处于锁的保护下。可以通过两种方式修复:
- 在锁内完成读取和打印
修改inc函数,把打印逻辑移到锁的范围内:
func (c *Container) inc(name string) { c.mu.Lock() defer c.mu.Unlock() c.counters[name]++ fmt.Println(name, c.counters) // 打印操作放入锁保护 time.Sleep(time.Second) }
同时移除doIncrement里的打印语句。
- 添加带锁的读取方法
给Container新增一个安全读取counters的方法,返回副本避免外部并发访问原map:
func (c *Container) GetCounters() map[string]int { c.mu.Lock() defer c.mu.Unlock() counterCopy := make(map[string]int, len(c.counters)) for k, v := range c.counters { counterCopy[k] = v } return counterCopy }
然后在doIncrement中调用该方法打印:
doIncrement := func(name string, n int) { for i := 0; i < n; i++ { c.inc(name) fmt.Println(name, c.GetCounters()) } wg.Done() }
内容的提问来源于stack exchange,提问作者ericobi
相关产品推荐
相关产品推荐

