Go 语言程序设计——并发编程(1)

  • 并发编程可以让开发者实现并行的算法以及编写充分利用多处理器和多核性能的程序
  • 编写、维护和调试并发程序相比单线程程序而言要困难很多
  • Go语言的并发解决方案有3个优点:
    • Go语言对并发编程提供了上层支持,因此正确处理并发是很容易做到的
    • 用来处理并发的 goroutine 比线程更加轻量
    • 并发程序的内存管理有时候是非常复杂的,而Go语言提供了自动垃圾回收机制
  • Go语言为并发编程而内置的上层API基于CSP模型(Communicating Sequential Processes)
  • Go语言通过线程安全的通道发送和接受数据以实现同步
  • Go和其他语言一样也提供了对底层功能的支持。在标准库的 sync/atomic包 里提供了最底层的原子操作功能,包括相加比较交换操作
  • Go语言的sync包还提供了非常方便的底层并发原语:条件等待互斥量
  • Go语言推荐程序员在并发编程时使用语言的上层功能,例如 通道goroutine

关键概念

  • 在并发编程里,我们通常想将一个过程切分成几块,然后让每个 goroutine 各自负责一块工作
  • 我们将 main() 函数所在的 goroutine 称为 主goroutine,其他附加创建用来负责处理相应工作的 goroutine 简称为 工作goroutine
  • Go语言虽然通过上层的API来处理并发,但仍有必要去避免一些陷阱:
    • 主goroutine 退出后,其他的 工作 goroutine 也会自动退出,所以我们必须非常小心地保证所有 工作 goroutine 都完成后才让 主goroutine 退出
    • 容易发生 死锁,这个问题有一点和第一个陷阱是刚好相反的: 所有的工作已经完成了,但是 主 goroutine工作 goroutine 还存活,这种情况通常是由于工作完成了但是 主 goroutine 无法获得 工作 goroutine 的完成状态
    • 死锁的另一种情况就是,当两个不同的 goroutine(或者 线程)都锁定了受保护的资源而且同时尝试去获得对方资源的时候(Go语言里并不多,因为Go程序可以使用 通道 来避免使用锁)
  • 为了避免程序提前退出或不能正常退出,常见的做法是让 主goroutine 在一个 done通道 上等待,根据接收到的消息来判断工作是否完成了
  • 另外一种避免这些陷阱的办法就是使用 sync.WaitGroup 来让每个 工作goroutine 报告自己的完成状态(使用 sync.WaitGroup 本身也会产生死锁,特别是当所有 工作goroutine 都处于锁定状态的时候(等待接受通道的数据)调用 sync.WaitGroup.Wait()
  • 就算只使用 通道,在Go语言里仍然可能发生死锁:
    *假如我们有若干个 goroutine 可以相互通知对方去执行某个函数(向对方发一个请求),现在,如果这些被请求执行的函数中有一个函数向执行它的 goroutine 发送了一些东西,例如 数据,死锁就发生了
    两个或多个阻塞线程试图取得对方的锁

    一个试图用对自身的请求来服务于请求的goroutine
  • 通道为并发运行的 goroutine 之间提供了一种无锁通信方式(尽管实现内部可能使用了锁,但无需我们关心)
  • 当一个通道发生通信时,发送通道接受通道(包括它们对应的 goroutine)都处于同步状态
  • 默认情况下,通道是双向的,既可以往里面发送数据也可以从里面接收数据
  • 我们经常将一个通道作为参数进行传递而只希望对方是单向使用的,这个时候我们可以指定通道的方向
  • 在通道里传输布尔类型整型或者 float64 类型的值都是安全的,因为它们都是通过复制的方式来传送的,所以在并发时如果不小心大家都访问了一个相同的值,这也没有什么风险(发送字符串也是安全的,因为Go语言里不允许修改字符串
  • Go语言并不保证在通道里发送指针或者引用类型(如切片映射)的安全性,因为指针指向的内容或者所引用的值可能在对方接收到时已被发送方修改
  • 当涉及指针引用时,有以下方法可以解决:
    1. 我们必须保证这些值在任何时候只能被一个goroutine访问得到,也就是说,对这些值的访问必须是串行进行的
    2. 设定一个规则,一旦指针或者引用发送之后发送方就不会再访问它,然后让接收者来访问和释放指针或者引用指向的值
    3. 让所有导出的方法不能修改其值,所有可修改值的方法都不引出。这样外部可以通过引出的这些方法进行并发访问,但是内部实现只允许一个goroutine去访问它的非导出方法
  • Go语言里还可以传送接口类型的值,也就是说,只要这个值实现了接口定义的所有方法,就可以以这个接口的方式在通道里传输
  • 只读型接口的值可以在任意多个goroutine里使用(除非文档特别声明),但是对于某些值,它虽然实现了这个接口的方法,但是某些方法也修改了这个值本身的状态,就必须和指针一样处理,让它的访问串行化

推荐阅读更多精彩内容