今晚的订单
#01 · 第一晚
危机
一开门就涌进来八桌客人。副厨一个人,一单一单地做。汤在锅里煮着,人就干站在旁边等。
目标
1 秒内送出八份订单。
深夜食堂 同时
开业第一晚,十点整。门一开,客人就涌了进来。你把第一单的汤放上火,然后守在锅前,一直等到汤开。三分钟后端出第一碗,再开始做第二单。
尹瑞妍主厨看着计时器说:“汤煮着的时候,你的手在干什么?什么也没干。这就是今晚的问题。”
我们用 Python 模拟一下“等汤煮开”这件事。time.sleep(0.3) 的意思是“什么都不做,等 0.3 秒”。就当是把三分钟缩成了 0.3 秒。
import time
def make_order(n):
time.sleep(0.3) # 等汤煮开
return f"#{n}"
start = time.perf_counter()
done = [make_order(n) for n in range(1, 9)]
print(f"{time.perf_counter() - start:.2f}s", done)
# 2.40s ['#1', '#2', '#3', '#4', '#5', '#6', '#7', '#8']time.perf_counter() 是专门用来计时的时钟。读两次再相减,就得到中间经过了多少秒。每次运行,最后几位数字可能略有不同。计时类示例的结果永远只是“大约”。
厨房里的活分两种。
| 要等的活(I/O) | 手不停的活(CPU) | |
|---|---|---|
| 在厨房里 | 煮汤、烤炉烘烤、等送货 | 揉面、切丝 |
| 在程序里 | 网络响应、读文件、数据库 | 计算、压缩、图像处理 |
| 等的时候 | 人闲着 | 人一直在忙 |
并发能大幅节省时间,靠的是要等的活多。汤在煮的时候,再架上一口锅就行了。手不停的活必须真的多几个人才能变快,而 Python 在这里藏着一条规矩。这个留到第 8 课再说。
这两个词经常混着用,但意思不同。
并行是实现并发的一种方式。如果厨房里大多是要等的活,哪怕只有一双手,光靠并发也能快很多。在下面的时间线里,轮流切换“一个人按顺序”和“多个厨师”跑一跑。一根横条就是一份订单,横轴是时间。
厨房时间线
订单
6
厨师
3
烹饪
等待
已取消
失败
time.sleep,图上画的是每份订单实际开始和结束的时刻。如果订单长度都一样,总耗时就是“分到订单最多的那个厨师的订单数 × 一份订单的时间”。
import math
def estimate(orders, cooks, each=0.3):
rounds = math.ceil(orders / cooks) # 一个厨师最多分到几份订单
return round(rounds * each, 2)
for cooks in (1, 3, 4, 8, 16):
print(cooks, estimate(8, cooks))
# 1 2.4
# 3 0.9
# 4 0.6
# 8 0.3
# 16 0.3Python 里有三种同时做多件事的工具。今天只尝个味道,从下一课开始一个一个深入。
import threading
threads = [threading.Thread(target=make_order, args=(n,)) for n in range(1, 9)]
for th in threads:
th.start() # 八个厨师同时开工
for th in threads:
th.join() # 等所有人做完
# 约 0.30 秒import asyncio
async def make_order_async(n):
await asyncio.sleep(0.3) # 等的时候去看别的订单
return f"#{n}"
async def main():
return await asyncio.gather(*(make_order_async(n) for n in range(1, 9)))
print(asyncio.run(main()))
# ['#1', '#2', '#3', '#4', '#5', '#6', '#7', '#8'] (约 0.31 秒)| 工具 | 换成厨房来说 | 适合的活 | 课次 |
|---|---|---|---|
threading | 同一间厨房里多请几个厨师 | 要等的活、老代码 | 2~7 |
multiprocessing | 连冰箱都分开用的分店厨房 | 手不停的活(CPU) | 8 |
asyncio | 一个人跑几十张桌子的前厅经理 | 要等的活特别多的时候 | 9~11 |
主厨的提醒
多请厨师确实会变快。可厨房也会变吵。两个人同时往一块黑板上写数字,会怎么样?第 3 课你就亲眼看到了。
多请厨师是有代价的。
今晚的目标是 1 秒内送出八份订单。订单全是“要等的活”,那就让八个厨师一人负责一份。
start = time.perf_counter()
threads = [threading.Thread(target=make_order, args=(n,)) for n in range(1, 9)]
for th in threads:
th.start()
for th in threads:
th.join()
print(f"{time.perf_counter() - start:.2f}s")
# 0.30s主厨按停计时器。“从 2.4 秒到 0.3 秒。第一天来说,还不赖。可厨师变成八个以后,谁做了几份,由谁来数?”
营业日志 — 第一晚。8 份订单全是要等的活,由 8 个厨师分头负责,0.3 秒做完。明天正式招厨师。
make_order 里的 time.sleep(0.3) 换成一个累加数字的循环(手不停的活),再用线程跑,测一测有没有变快。如果结果很奇怪,第 8 课会告诉你原因。主厨的配方卡
要等的活(I/O)靠并发能大幅加速。手不停的活(CPU)需要真正的并行。
并发 = 轮流推进,并行 = 同一时刻执行。
计时:start = time.perf_counter() … time.perf_counter() - start
三种工具:threading(多个厨师)、multiprocessing(多间厨房)、asyncio(一个人跑的前厅)。
并发不是免费的:管理成本和新的 bug 会随之而来。
订阅新课时和新课程通知
本课程新增课时或推出后续课程时,我们会发邮件通知你。
问答
关于本课有任何疑问都可以在这里留言。所有语言版本页面的提问会汇集在一起。