面试必问?一文带你速通Go语言并发

147 阅读5分钟

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}} 数据发入通道
      • 发送将持续阻塞, 直到数据被接受
      • 也就是说, <- 这个语句, 直到发送的数据被其他线程消费了之后, 才会执行下一句
  • 通道接收数据
    • 接收方必定在另一个进程, 因为发送方阻塞了
    • 没有发送方发送数据时, 接收方也会阻塞
    • 每次直接接受一个元素
    • 接收数据写法:
      • Data <- ch 阻塞接受, 执行时阻塞
      • Data,ok <- ch 非阻塞接受, 未接受到时, data为零且ok表示未接受到数据
      • <- ch 接受数据, 但是忽略, 不使用
        • 只是用于做并发同步
      • 通道可以使用range遍历

单向通道

  • 只能读 或者 写 (只是使用限制)

Pasted Graphic 4.png

  • 关闭通道
    • close(ch)

无缓冲的通道

  • 接受前没有能力保存任何值, 要求发送方和接收方同时准备好

Pasted Graphic 5.png

带缓冲的通道 buffered channel

  • 在接受前能存储一个或多个值的通道, 不要求发送与接收的同步
  • 创建方法:
    • 在创建时, make的参数添加一个int值表示缓冲大小 ch := make(chan int, 5)
  • 阻塞条件
    • 无缓冲通道可以认为是缓冲区长度为0的通道
      • 通道填满时, 发送方阻塞
      • 通道为空时, 接收方阻塞

Channel 的超时机制

  • 用select来设置超时
  • select语法类似switch

Pasted Graphic 6.png

  • 没有default时会阻塞

Pasted Graphic 7.png

互斥锁 和 读写锁

  • sync.Mutex
    • 互斥锁
  • sync.RWMutex
    • 单写多读
    • 读锁占用时阻止写, 但不阻止读
    • 写锁阻止读写
  • 每一个Lock() or RLock() 都要有对应的Unlock( ), 不然会死锁

等待组 WaitGroup

  • Channel, 共享内存加锁以外的并发处理方式
  • sync.WaitGroup
  • 多个任务间的同步, 在并发环境中完成指定数量的任务
  • 调用方法, 维护组内置的计数器,
    • 添加任务, 计数器加一
    • 完成任务, 计数器减一
    • 主函数中, waitGroup.wait( ), 等待计数器降为0

死锁、活锁、饥饿

  1. 死锁 (多个进程互卡)
    1. 两个进程相互等待, 无法推进
      1. 发生条件:
        1. 互斥条件
        2. 请求和保持条件
        3. 不剥夺条件
        4. 环路等待条件
      2. 解决办法:
        1. 并发查询多个表时, 约定访问顺序
        2. 一个事务中尽可能一次性获取所需资源
        3. 易死锁场景使用表级锁
        4. 乐观锁/分布式事务锁
  2. 活锁 (失败进程一直重复)
    1. 不阻塞, 但是一直失败
    2. 处理消息的失败回滚事务, 有时候会导致活锁
    3. 解决方法: 重试机制加入随机性, 例如CSMA/CD
    4. 活锁有可能自行解开
  3. 饥饿
    1. 能运行的进程被调度器忽略, 不能执行
    2. 解决方法: 降低锁的使用粒度