理论
线程虽然能并发,但线程的创建与切换本身也有代价(8MB的栈,上下文切换),创建线程的代价有可能比函数调用还要大。
问题是:线程虽然比进程轻,但还是太重,怎么更轻,还能支持大量并发任务?
对于这种情况,一种思路是设计出更轻量的线程,一种思路是事件驱动异步编程模型。
先看第一种思路。
import random
THREADS = 1_000_000
def T_worker(name): i = 0 while (i := i + 1): yield f"[{name}] i = {i}"
threads = [T_worker(i) for i in range(THREADS)]
count = 0
while count < 10_000_000: current = random.choice(threads) res = current.send(None) # 等价于 res = next(current) print(res) count += 1Python的yield语法就像是一个生产用户态的简易协作式线程的工具(含有yield的函数是一个generator),当generator运行到yield时就暂存当前函数的上下文,然后主动让出时间片,这里的random就像是一个调度器。
但这本质是在用户态模拟“并发”,由第三方线程库的调度逻辑或者编程语言自己的运行时来调度,内核是感知不到这些用户线程的,所以没有物理并行,内核眼里还是只有一个Python线程在跑,对于CPU密集的任务作用不大。
但对于I/O密集的任务,这种用户态线程配合非阻塞的I/O API是很有作用的。
task A:CPU 1msread syscallCPU 1ms
taskB:...假设有这样一个A任务,如果用默认的阻塞式read,没有东西读就等着,那用户线程跑到这里的时候已经陷入内核开始等待,也无法yield以后让实际的内核线程去跑B任务。
但read其实可以开启O_NONBLOCK非阻塞模式(OS提供了),没读到会立即返回一个特殊值EAGAIN。那代码逻辑里其实就可以写好出现这种情况就yield,先让B任务跑起来。
TIP那么对于非阻塞的API,到底什么时候才该回来读呢? 如果只是过一会就回来读,重复
read → EAGAIN → yield的这个过程,那就还是轮询,并且这里的“一会”是多久也很难确定。 如果能把I/O对象设计成fd,就可以用epoll来监听了! 所以流程应该是read → EAGAIN → 注册fd到epoll → yield → 调度器去跑任务B ... 某一刻fd变成ready → epoll_wait()返回这个fd → 调度器去唤醒任务A
这种需要自己写调度器,控制用户线程间如何yield的模型对程序员的心智负担还是有点大的。
如果能让程序员仍然按照传统同步阻塞的线程模型写代码,而由语言运行时负责轻量线程的创建、调度、阻塞与唤醒,就可以进一步降低并发编程的负担。Go 的 goroutine 基本就是这条路线。
func worker() { data := readSomething() result := process(data)}
go worker()Go就是这样的语言,这看起来完全是同步阻塞代码,但Go的运行时不会让这个goroutine在read处阻塞,而是让它睡去,等有消息了再唤醒,背后和epoll那套是一样的。
并且Go的运行时会维护若干内核线程与若干goroutine之间的映射,实现物理上的并行。
Go隐藏了用户线程调度的细节,隐藏了异步I/O的实现机制,但程序员还是需要处理很多并行流,此时就需要用上互斥与同步。
再看第二种事件驱动的异步编程模型思路。
JavaScript是这种思路的实践者,因为JavaScript服务于网页,而网页会面临很多事件,比如鼠标不知道什么时候会被点击,网页请求不知道什么时候会结束。
浏览器需要协调用户输入、脚本执行、网络、定时器和渲染等大量不同时到来的事件,因此使用 event loop 组织这些任务。
read(fd, callback);在JavaScript里,一般都使用非阻塞的I/O API。
这样的一个read实际上的行为如下。
1. 我要读 fd2. 现在数据还没准备好3. js runtime 记住: fd 123 ready 之后 要执行 callback4. 当前 JS 函数返回5. JS 继续做别的事情在单线程event loop中,一个同步callback一旦开始执行,会run-to-completion,其他callback不会从中间插入,因此普通callback之间通常不需要mutex来防止线程抢占。
TIP什么是event loop? event loop 不是 JS 语法的一部分,而是浏览器/Node 这样的 runtime 提供的调度机制。 可以粗略理解为:
while (true) {从队列拿一个事件执行它对应的 JS callbackcallback 执行完再拿下一个}传统 JavaScript 的主执行模型是单线程的。大概是这样的模型:
event queue┌────────────────────┐│ callback A ││ callback B ││ callback C │└──────────┬─────────┘↓event loop↓一个 JS 执行线程↓CPU
readFile("a.txt", function (err, data) { if (err) { console.error(err); return; }
console.log(data);
// 读完以后要做的后续事情 process(data);});
console.log("我不用等 read 完成");本质上回调函数里就是read结束以后应该继续做什么。
所以其实原始的事件驱动的心智模型很简单,由语言的运行时去决定什么时候回来执行这段I/O之后的逻辑。
readA(function (a) { readB(a, function (b) { readC(b, function (c) { process(c); }); });});但这样的写法会容易造成回调地狱,写出不好阅读的代码。
“未来的结果”和“结果出来以后做什么”现在全部通过函数嵌套表达,控制流越来越难读。
Promise/Future其实是把“未来的结果”抽象成对象。
readA(function(a) { process(a);});变成
const p = readA();p并不是a,而是一个未来可能会产生结果的对象,目前是pending,未来可能变成fulfilled(a)或rejected(error)。
readA() .then(function(a) { return readB(a); }) .then(function(b) { return readC(b); }) .then(function(c) { process(c); });代码变成这样。
但程序员还是想像写同步程序一样写异步程序。
const a = readA();const b = readB(a);const c = readC(b);process(c);类似这种东西。
于是就有了async/await语法糖:
async function work() { const a = await readA(); const b = await readB(a); const c = await readC(b);
process(c);}看起来像同步代码,但实际上语义是:
启动 readA↓结果没好↓暂停当前 async function↓把控制权还给 event loop↓event loop 跑其他任务↓Promise 完成↓恢复这个 async function(从上次暂停那一行开始)Python的async/await也是类似的东西。
import asyncio
async def worker(): print("A")
data = await read_something()
print("B") process(data)执行流大概如下。
worker 运行↓print("A")↓await read_something()↓结果还没好↓暂停 worker coroutine↓控制权回到 event loop↓event loop 去运行别的 coroutine↓read 完成↓event loop 再恢复 worker↓data = 结果↓print("B")和JavaScript的区别是Python里这个event loop由asyncio这个库提供,而不是语言运行时提供。
while True: 找一个 ready 的 coroutine 恢复它运行
它: return 或者 await
回到 event loop这个event loop大概可以这么理解。
import asyncio
async def worker(name): print(name, "start")
await asyncio.sleep(1)
print(name, "end")
async def main(): await asyncio.gather( # 一下子丢多个coroutine给event loop worker("A"), worker("B"), worker("C"), ) # await worker("A") # await worker("B") # await worker("C") # 这样就是3秒
asyncio.run(main())这个例子程序的输出是A start B start C start 1秒 A end B end C end。
async def → 定义 coroutine
await X → 当前 coroutine 等 X,期间可以让 event loop 跑别人
create_task(X) → 把 coroutine X 注册成可以独立调度的 Task
gather(A, B, C) → 一起调度 A/B/C,并等待它们全部结束
asyncio.run(main()) → 创建/运行 event loop,从 main 开始跑Python asyncio的几个语义。