跳转到内容

第四部分导读:并发

一个程序里常常有好几件事要同时进行。一种是等:发出三个网络请求后,CPU 只是干等回应,不如三个一起发、一起等。另一种是算:一张大图要压缩,机器有八个核,只用一个就浪费了七个。这一部分讲的共同问题是:一个程序怎样同时做几件事,以及四种运行时的根本差别。

差别要从运行时说起。Python 和 JS/TS 以单线程事件循环加协程为主,想用多核就得另开进程或 Worker;C# 把 Task 放到线程池上,线程可以并行;Go 由运行时把 goroutine 调度到系统线程上。这个出发点不同,后面各章的写法也跟着不同。同样是共享一个计数器,Python、C# 和 Go 的线程默认共享内存,要靠锁保护,JS 默认什么也不共享。同样是“等一次查询不占住线程”,Python、JS/TS 和 C# 用 async/await,Go 没有这个语法。同样是让任务停下来,四种语言都是协作式取消,却各有各的载体。

建议先读第 16 章,它把并发与并行、进程、线程、协程这些概念,以及四种运行时模型讲清楚,后面五章都在这个基础上展开。之后按需要选读:碰到共享内存读第 17 章,需要多核或隔离读第 18 章,写异步代码读第 19 章,要让几个部分交换数据读第 20 章,要设超时、要停任务读第 21 章。异步所需的能中途暂停再恢复的函数,就是第 13 章讲的生成器和迭代器状态机。

章 一句话 四种语言差异最大的一点
第 16 章 并发总览 同时等或同时算好几件事,怎么安排才不白白浪费等待和核 能否用多核:C# 和 Go 用平常的并发写法就能分到多个核,Python 和 JS/TS 要换成进程或 Worker,数据也要复制过去
第 17 章 多线程 几条线程读写同一块内存时,让结果不随先后而变 默认能否共享内存:Python、C# 和 Go 默认什么都共享、要你主动保护,JS 默认什么都不共享、要你主动开一块共享区
第 18 章 多进程 把工作交给另一个进程,再把结果拿回来 序列化:Python 和 JS/TS 自动打包对象,C# 和 Go 只给字节管道,格式自己定
第 19 章 异步 等待时不占线程,又能写成顺序代码 阻塞调用的后果:同一个 sleep,在 Python 和 JS 的异步代码里会卡住所有任务,在 C# 里只浪费一条线程,在 Go 里就是正确的写法
第 20 章 通道与消息传递 不共享变量、不加锁,靠发一条收一条串起并发的部分 多路等待:只有 Go 有 select 语句,另外三种语言要把等待包成任务或 Promise 再求最先完成的那个
第 21 章 取消、超时与结构化并发 让不再需要的任务真的停下来,函数返回时它启动的任务都已结束 结构化并发:只有 Python 的标准库提供了现成的结构化作用域(TaskGroup),另外三种语言要靠程序员每次都记得等

下面这张图列出这部分的六类问题,以及各自该翻的章。

遇到的问题与该看的章

图的左列是你可能遇到的问题,右列是该先翻的章。

遇到什么问题先看哪章:

  • 你想知道该用线程、协程还是进程,或者想弄清并发与并行的区别 → 看第 16 章 并发总览。
  • 你想让几条线程读写同一块内存,结果还不乱 → 看第 17 章 多线程。
  • 你想用上多个核,或者想让一段代码和主程序隔离 → 看第 18 章 多进程。
  • 你想等一次查询时不占住线程,又把“先查这个、再查那个”写成顺序代码 → 看第 19 章 异步。
  • 你想让并发的几个部分交换数据,并让下游知道上游什么时候结束 → 看第 20 章 通道与消息传递;想给任务设超时、让它们真的停下来 → 看第 21 章 取消、超时与结构化并发。