一 前言
本文算是对《Go并发编程实战》一书的小结。
我们谈go的优点时,并发编程是最重要的一块。因为go基于新的并发编程模型:不用共享内存的方式来通信,作为替代,以通信作为手段来共享内存。(goroutine共享channel,名为管道,实为内存)。
为解释这个优势,本文提出了四个概念:交互方式、手段、类型、目的(不一定对,只是为了便于描述)。并在不同的并发粒度上(进程、线程、goroutine)对这几个概念进行了梳理。
几个概念
不同层面的并行化支持 | 表现 |
---|---|
硬件并行化 | 多核心、多cpu |
操作系统并行化 | 并行抽象(进程、线程)以及交互手段的提供 |
编程语言并行化 | java启动一个线程要extends thread,而go只需一个go func(){} |
并发编程思想来自多任务操作系统,随后,人们逐渐将并发编程思想凝练成理论(操作系统的几个基本部分:进程、内存、存储、IO,进程部分其实很厚的),同时开发出了一套关于它的描述方法。之后,人们把这套理论融入到编程语言当中。
并发程序内部有多个串行程序,串行程序之间有交互的需求,交互方式有同步和异步;不同的方式有各自的交互手段(整体分为共享内存和通讯两个交互类型);交互目的分为:互斥访问共享资源、协调进度。彼此的关系是:交互手段有交互方式和类型两个维度,一个交互方式可以实现多个交互目的(异步无法实现共享资源的互斥访问)。
交互目的中,比较重要的是对共享资源的访问。这个共享资源,对进程是文件等,对线程是共享内存等。共享就容易发生干扰,一切问题的起点是:OS为了支持并发执行,会中断进程==> 中断前要保存现场,中断后要恢复现场。这个现场小了说是寄存器数据,大了说,就是进程关联的所有资源的状态(比如进程打开的fd) ==> 要确保进程“休息”期间,别的进程不能“动它的奶酪”,否则,现场就被破坏了。==> 解决办法有以下几个:
- 访问共享资源的操作不能被中断(原子操作);
- 可中断,但资源互斥访问(临界区);
- 资源本身就是不变的(比如常量)
临界区为什么不都弄成原子操作呢?因为一个操作执行起来没办法中断,也意味着很大的风险。所以,内核只提供针对二进制位和整数的原子操作。
交互类型中,通信比共享内存要简单。因为,把数据放在共享内存区供多个线程访问,这种方式的基本思想非常简单,却使并发访问控制变得复杂,要做好各种约束和限制,才能使看似简单的方法得以正确实施。比如,当线程离开临界区时,不仅要放弃对临界区的锁定(设置互斥量),还要通知其它等待进入该临界区的线程(操作条件变量)。同步工具的引入(互斥量和条件变量等)增加了业务无关代码,其本身的正确使用也有一定的学习曲线。而通讯就简单了,收到消息就往下走,收不到就等待,自得其乐,不用管其它人。
针对并发粒度的不同,我们把上述概念梳理一下:
并发粒度 | 交互手段 | 同步/异步 | 交互类型 |
---|---|---|---|
进程 | 管道、信号、socket | 同步异步都有 | 只支持通信 |
线程 | 共享内存 | 1. 互斥量+条件变量 支持同步;2. 程序层面通过模拟signal弄出的futrue模式支持异步 | 只支持共享内存,高层抽象支持通信,比如java的blockingQueue |
goroutine | channel | 1. channel支持同步;2. 程序层面提供异步 | 只支持通信,高层抽象支持共享内存,比如go的sync包 |
PS,routine is a set sequence of steps, part of larger computer program.
go为什么更好的支持并发
- 两级线程模型。为什么在线程之上再弄出一层呢?程序只需设定并发指定体就好(即程序只需描述哪些代码需要并发),具体的并发执行由go runtime负责。
- 语言层面的支持。java启动一个线程要extends thread,而go只需一个
go func(){}
- 基于通信的并发编程模型。好处见上文。