sync
包是Go
标准包之一, 其使用场景可以分为三种:
Mutex
: 共享锁, 读写锁用于共享资源的互斥使用;WaitGroup
:goroutine
同步控制;Once
: 全局唯一初始化代码.
在并发编程过程中, 涉及到前两种使用场景. 本文会简单采用Demo
方式介绍Mutex, WaitGroup
的使用.
Demo
Step1:
首先我们将定义一个并发不安全的计数器, 在单协程的场景下运行, 并检验其结果, 示例代码如下:
1 | package step1 |
输出为: Assert SUCCESS!!!
上面的代码中首先定义了一个计数器Counter
, 其存在两个方法Inc
:计数自增方法, Value
:获得计数方法, 这是一种并发不安全的定义写法. 其次定义了一个assertCount
工具类函数, 用于检验计数器计数是否与预期一致;最后在Assert
函数, 通过for loop
的方式调用了10次自增方法, 随后检验是否与预期计数一致. 结果是预期一致的;
Step2:
单协程运行自然是安全而稳定的, 但是对于性能表现必然是不及多并发环境的, 为了验证并发前后的性能差距, 我这边编写了benchmarkTest
:
1 | func BenchmarkAssert(b *testing.B) { |
一次执行结果如下:
1 | go test -bench . |
执行次数10
次, 每次时间开销为101500306 ns
, 总时间开销为1.230s
;
添加sync并发控制代码: 启动10个goroutine
去执行:调整Assert
函数如下:
1 | func Assert() { |
在上面的示例函数中, 我们利用sync.WaitGroup
实现了goroutines
的同步控制:
1 | var wg sync.WaitGroup // 声明一个waitgroup变量, 声明之后即可使用, 无需特殊初始化 |
再次执行benchmark
:
1 | go test -bench . |
发现: 执行次数为100
次, 每次执行时间开销为10211660 ns
纳秒, 总时间开销为1.046s
.发现性能表现显著提升.但是, 如果将Assert
函数中的计数次数提升, 会发现其并发并不安全: 例如我将协程更改为1000次, 预测结果为1000的话:
1 | func Assert1000() { |
1 | go test -v . |
这是由于多个协程对于共享资源counter.Count
的访问时并发不安全的, 需要借助于sync.Mutex / sync.RWMutex
, 进行资源访问并发控制;
Step3:
sync.Mutex
是互斥锁, 适用于写多读少场景. sync.RWMutex
是读写锁, 适用于读多写少的场景;本用例的场景为写多, 所以可以采用sync.Mutex
, 调整后代码如下:
1 | package step3 |
我们在Counter
中添加了属性mu sync.Mutex
, 用于控制Counter
的同步访问. 在访问Counter
前, 通过c.mu.Lock()
加锁, 访问结束后通过c.mu.Unlock()
解锁;再次执行测试函数:
1 | go test -v . |
发现执行通过.
需要注意的是:
sync.Mutex
在声明后是不允许复制的, 这是因为值传递的效果导致出现了另一把锁, 会使得预期不符.unbuffered channel
也可以做为所出现, 那么什么情况下需要使用mutex
?1
2
3Paraphrasing:
• Use channels when passing ownership of data
• Use mutexes for managing state- 当传递的是数据的所有权时建议使用
channel
- 当传递的时数据的状态时建议使用
mutex
- 当传递的是数据的所有权时建议使用
结论
本文总结了在并发编程中sync
的使用场景, 一共分为两种:
sync.Waitgroup
用于goroutine
同步控制;sync.Mutex
用于资源的互斥访问.
并通过Demo
的方式一步一步的展示了两种使用方式, 理解起来应该是简单的;
在实际项目中, channel
, sync
在很多场景的作用时相同的, 既可以使用channel
,也可以使用sync
,甚至会出现混用的情况.个人认为不需要纠结, 只需要编写符合项目风格, 可读性更好的代码即可;