Go语言并发
#Go #并发编程
参考 c.biancheng.net/view/4356.h…
简述
- 概念
- 进程/线程
- 一个进程能创建和撤销多个线程
- 并发/并行
- 多线程在单核CPU =》 并发
- 多线程在多核CPU =〉 并行
- 一个线程可以跑多个协程, 协程是轻量级的线程
- 进程/线程
- goroutine
- 所有的goroutine会在main() 结束时一同结束
- channel
- goroutine间通信的方式
- 跨进程的通信是使用分布式系统实现
- 一个channel只能传递一个类型的值, 在声明时指定 (类型安全)
- 操作系统在调用进程时, 会保存被调进程的上下文环境, 后面再恢复
- 并发的应用场景
- 图形化界面, 一边处理请求一边渲染图像
- Web应用处理大量请求
- 分布式系统的多核使用
- I/O 阻塞时, 还有其他与I/O无关的操作需要执行
并发通信
- 两种模型
- 共享数据
- 多个并发单元分别保持对同一个数据的引用, 实现共享
- 常见例子: 内存
- 问题在于: 这种通信方式, 写出来的代码十分臃肿, 有大量锁相关的代码
- 消息
- goroutine采用了消息的模式
- 消息认为每个并发单元是独立的个体, 有自己的变量且不共享
- 通过channel提供消息通信机制
- 共享数据
竞争状态
- 不同goroutine之间没有相互同步时, 访问共享资源时会产生冲突, 需要同步保护
- 对同一个资源的读写必须是原子化的, 同一时间只能有一个goroutine对共享资源读写
- ·Go build -race· 指令生成的程序自带检测资源竞争的功能
- 传统的解决办法
- 用原子函数操作(atmoic包)
- Sync包的锁机制 (互斥锁)
- 使同一时间只有一个goroutine能进入
GoMAXPROCS 调整性能
- 如果要手动维护线程与CPU核心的话, 使用
runtime.GOMAXPROCS( 逻辑CPU数量 ) - 可以用
runtime.NumCPU( )查询CPU数量, 配合GOMAXPROCS函数最大化利用核心数
并发(concurrency) 与并行(parallelism)
- 并发同一时刻, 任务实际上不是同时进行的
- 并行同一时刻, 任务实际上是同时进行的
- 并发能以较少的资源做更多事
Goroutine 与 Coroutine
- goroutine可能并行, 而coroutine始终是顺序执行
- goroutine使用通道来通信, 而coroutine使用让出yield和恢复resume来通信
Channel
- Channel是一个类似于队列的结构, FIFO 保证收发顺序
- 同一时间只有一个goroutine 访问通道进行发送
- 使用一个通道发送数据
- ch <- {{message}} 数据发入通道
- 发送将持续阻塞, 直到数据被接受
- 也就是说, <- 这个语句, 直到发送的数据被其他线程消费了之后, 才会执行下一句
- ch <- {{message}} 数据发入通道
- 通道接收数据
- 接收方必定在另一个进程, 因为发送方阻塞了
- 没有发送方发送数据时, 接收方也会阻塞
- 每次直接接受一个元素
- 接收数据写法:
- Data <- ch 阻塞接受, 执行时阻塞
- Data,ok <- ch 非阻塞接受, 未接受到时, data为零且ok表示未接受到数据
- <- ch 接受数据, 但是忽略, 不使用
- 只是用于做并发同步
- 通道可以使用range遍历
单向通道
- 只能读 或者 写 (只是使用限制)
- 关闭通道
- close(ch)
无缓冲的通道
- 接受前没有能力保存任何值, 要求发送方和接收方同时准备好
带缓冲的通道 buffered channel
- 在接受前能存储一个或多个值的通道, 不要求发送与接收的同步
- 创建方法:
- 在创建时, make的参数添加一个int值表示缓冲大小 ch := make(chan int, 5)
- 阻塞条件
- 无缓冲通道可以认为是缓冲区长度为0的通道
- 通道填满时, 发送方阻塞
- 通道为空时, 接收方阻塞
- 无缓冲通道可以认为是缓冲区长度为0的通道
Channel 的超时机制
- 用select来设置超时
- select语法类似switch
- 没有default时会阻塞
互斥锁 和 读写锁
- sync.Mutex
- 互斥锁
- sync.RWMutex
- 单写多读
- 读锁占用时阻止写, 但不阻止读
- 写锁阻止读写
- 每一个Lock() or RLock() 都要有对应的Unlock( ), 不然会死锁
等待组 WaitGroup
- Channel, 共享内存加锁以外的并发处理方式
- sync.WaitGroup
- 多个任务间的同步, 在并发环境中完成指定数量的任务
- 调用方法, 维护组内置的计数器,
- 添加任务, 计数器加一
- 完成任务, 计数器减一
- 主函数中, waitGroup.wait( ), 等待计数器降为0
死锁、活锁、饥饿
- 死锁 (多个进程互卡)
- 两个进程相互等待, 无法推进
- 发生条件:
- 互斥条件
- 请求和保持条件
- 不剥夺条件
- 环路等待条件
- 解决办法:
- 并发查询多个表时, 约定访问顺序
- 一个事务中尽可能一次性获取所需资源
- 易死锁场景使用表级锁
- 乐观锁/分布式事务锁
- 发生条件:
- 两个进程相互等待, 无法推进
- 活锁 (失败进程一直重复)
- 不阻塞, 但是一直失败
- 处理消息的失败回滚事务, 有时候会导致活锁
- 解决方法: 重试机制加入随机性, 例如CSMA/CD
- 活锁有可能自行解开
- 饥饿
- 能运行的进程被调度器忽略, 不能执行
- 解决方法: 降低锁的使用粒度