原子操作就是并发编程中“最小的且不可并行化”的操作。
1.一般情况下,原子操作都是通过“互斥”访问来保证的
sync.Mutex
用互斥锁来保护一个数值型的共享资源,麻烦且效率低下。
- sync/atomic 包对原子操作提供了丰富的支持
sync/atomic 包对基本的数值类型及复杂对象的读写都提供了原子操作的支 持。 atomic.Value 原子对象提供了 Load 和 Store 两个原子方法,分别用于加 载和保存数据,返回值和参数都是 interface{} 类型,因此可以用于任意的自定 义复杂类型。
3.sync.Once
原子操作配合互斥锁可以实现非常高效的单件模式。互斥锁的代价比普通整数的原子读写高很多,在性能敏感的地方可以增加一个数字型的标志位,通过原子检测标 志位状态降低互斥锁的使用次数来提高性能。
4.顺序一致性内存模型
如果只是想简单地在线程之间进行数据同步的话,原子操作已经为编程人员提供了一些同步保障。不过这种保障有一个前提:顺序一致性的内存模型。
不同的Goroutine之间,并不满足顺序一致性内存模型,需要通过明确定义的同步 事件来作为同步的参考。
原子操作并不能解决问题,因为我们无法确定两个原子操作之间的顺序。 解决问题的办法就是通过同步原语来给两个事件明确排序
go 内写入channel ,main读取channel来产生同步
5.基于channel的通信
若在关闭Channel后继续从中接收数据,接收者就会收到该Channel返回的零值。
因此用 close© 关闭管道代替 done <- false 依然能保证该程 序产生相同的行为。
我们可以根据控制Channel的缓存大小来控制并发执行的Goroutine的最大数目
6.Go语言中常见的并发模式
CSP 通讯顺序进程
作为Go并发编程核心的CSP理论的核心概念只有一个:同步通信。
不要通过共享内存来通信,而应通过通信来共享内存。=>通过管道来传值是Go语言推荐的做法
(虽然像引用计数这类简单的并发问题通过原子操作或互斥锁就能很好地实现,但是通过Channel来控制访问能够让你写出更简洁正确的程序。)
等待N个线程完成后再进行下一步的同步操作有一个简单的做法,就是 使用 sync.WaitGroup 来等待一组事件
生产者消费者
简单的channel读写
// Ctrl+C 退出 sig := make(chan os.Signal, 1) signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM) fmt.Printf(“quit (%v)\n”, <-sig)
发布订阅模型 pub/sub
控制并发数
启动并发前先写channel,满了则阻塞
并发的安全退出
基于 select 实现的管道的超时判断
通过 select 的 default 分支实现非阻塞的管道发送或接收操作
通过 close 来关闭 cancel 管道向多个Goroutine广播退出的指令。
可以结合 sync.WaitGroup 来让goroutine在退出前做一些清理工作
7.context包
用来简化对于处理单个请求的 多个Goroutine之间与请求域的数据、超时和退出等操作
超时退出改进
ctx, cancel := context.WithTimeout(context.Background(), 10* time.Second)
。。。
cancel()
goroutine
case <-ctx.Done(): return ctx.Err()