之前用 go 写一个小工具的时候, 用到了多个协程之间的通信, 当时随手查了查, 结果查出来一大坨, 简单记录一下.
golang
中多个协程之间是如何进行通信及数据同步的嘞.
共享变量
一个最简单, 最容易想到的, 就是通过全局变量的方式, 多个协程读写同一个变量. 但对同一个变量的更改, 就不得不加锁了, 否则极易引发数据问题. 一般系统库都提供基本的锁, go 也提供了.
package mainimport (\"fmt\"\"sync\"\"time\")var num = 0// 互斥锁var mutex = sync.Mutex{}// 读写锁var rwMutex = sync.RWMutex{}func main() {for i := 0; i < 100; i++ {go incrNum()}time.Sleep(2)fmt.Println(num)}func incrNum() {mutex.Lock()num = num + 1mutex.Unlock()}
仅执行一次
当查询锁查到
sync
这个模块时, 发现它下面的对象并没有几个, 都是针对协程同步的各个方面给出的解决方案. 所以我就一个一个看文档试了试.
当你需要对环境, 连接池等等资源进行初始化时, 这种操作只需要执行一次, 这时候就需要它了.
sync.Once
对象可以保证仅执行一次. 和 init 方法有些类似, 不过 init 方法是在模块首次加载时执行, 而
sync.Once
是在首次调用时执行. (其实现就是一个计数器加一个互斥锁)
package mainimport (\"fmt\"\"sync\"\"time\")var num = 0var once = sync.Once{}func main() {for i := 0; i < 100; i++ {go once.Do(incrNum)}time.Sleep(2)fmt.Println(num)}func incrNum() {num = num + 1}
等待其他协程处理
某个协程需要等第一阶段的所有协程处理完毕, 才能开始执行第二阶段. 这个时候, 等待其他协程就可以通过
sync.WaitGroup
来实现. (当然, 也可以通过一个共享计数器变量来实现).
package mainimport (\"fmt\"\"sync\")var waitGroup = sync.WaitGroup{}func main() {for i := 0; i < 100; i++ {go incrNum()}// 等待其他协程处理完毕(共享变量为0)waitGroup.Wait()fmt.Println(\"don\")}func incrNum() {// 增加需要等待的协程数量(共享变量+1)waitGroup.Add(1)// do something// 标记当前协程处理完成(共享变量-1)waitGroup.Done()}
消息通知
多个协程启动时, 等待某个命令到来时执行命令, 唤醒等待协程. go 对此类操作也进行了处理, 感觉好贴心哦. 但是经过测试, 即使没有空闲的协程, 唤醒命令同样能够发出去, 所以需要注意一下.
package mainimport (\"sync\")var mutex = &sync.Mutex{}var cond = sync.NewCond(mutex)func main() {for i := 0; i < 100; i++ {go incrNum()}// 发送命令给一个随机获得锁的协程cond.Signal()// 发送命令给所有获得锁的协程cond.Broadcast()}func incrNum() {// 获取锁, 标识当前协程可以处理命令cond.L.Lock()// 可添加退出执行命令队列的条件for true {// 等待命令cond.Wait()// do something}// 释放锁, 标记命令处理完毕, 退出协程cond.L.Unlock()}
多协程 map
普通的 map 在多协程操作时, 是不支持并发写入的.
go
贴心的给封装了支持并发写入的
map
. 同时也提供了针对
map
的基本操作.
package mainimport (\"fmt\"\"sync\"\"time\")var m = sync.Map{}func main() {for i := 0; i < 100; i++ {go func() {m.Store(\"1\", 1)}()}time.Sleep(time.Second * 2)// 遍历 mapm.Range(func(key, value interface{}) boad8ol {// 返回 false 结束遍历return true})// 读取变量, 若不存在则设置m.LoadOrStore(\"1\", 3)// 删除 keym.Delete(\"1\")// 读取变量load, _ := m.Load(\"1\")fmt.Println(load)}
多协程对象池
对于数据库连接池应该并不陌生. 而
sync.Pool
对象是
go
封装的协程安全的对象池. 对象池的使用十分简单, 存/取
package mainimport (\"sync\")var p = sync.Pool{// 当池子中没有对象了, 用于创建新对象New: func() interface {}{return \"3\"},}func main() {// 从池子中获取一个对象r := p.Get()// 用完后将对象放回池子中p.Put(r)}
sync 简单总结
针对
go
系统的
sync
模块, 提供的基础功能如下:
- 互斥锁
Mutex
- 读写锁
RWMutex
- 函数单次执行
Once
- 协程执行等待
WaitGroup
- 协程消息通知
Cond
- 多协程 map
Map
- 多协程对象池
Pool
几个都简单试过之后, 发现
sync
模块针对常用的几个多协程工具进行了封装, 想来可以基本满足日常使用了.
终极通信-channel
channel
是一个协程安全的通信管道, 简单理解为数据从一侧放入, 从另一侧拿出. 这玩意感觉能玩出花来, 还不太理解, 留到国庆研究.