📄 一句话概括
实测算数任务 4 线程几乎没快(都是 0.03 秒左右),等待任务快了 4 倍(0.8 秒降到 0.2 秒)——线程的价值在【等】上。
📝 本节和前面几节的关系
5.3 讲了虚拟机怎么跑代码,这一节讲同时跑几份是怎么做到的。
一、线程是什么
一个进程里可以有多条执行线,它们共享同一个堆。 共享堆就是共享对象——所以多个线程能同时改同一个对象,这正是麻烦的来源。 每条线程有自己的栈:各自的局部变量、各自的调用链。
二、实测两类任务
算数(吃 CPU):单线程跑 4 份和 4 线程各跑 1 份,都是 0.03 秒左右——几乎没快。 等待(不吃 CPU):单线程 0.8 秒,4 线程 0.2 秒——快了 4 倍。 同样是【4 个线程】,差距却是天壤之别。
三、为什么:GIL
Python 有 GIL(全局解释器锁):同一时刻只有一个线程能执行字节码。
所以吃 CPU 的活儿,多线程只是轮着跑,总时间不变。
等待的活儿不一样:sleep 时线程主动让出 GIL,别人就能跑——真的重叠了。
四、该用线程还是进程
IO 密集(读文件、发请求、等数据库):用线程,等待时可以重叠。 CPU 密集(算、压、加密):用多进程,绕开 GIL 真正并行。 判断标准只有一条:这段代码的时间花在【等】上还是花在【算】上。 线程还带来一个新麻烦:共享堆上的对象谁先改。这是并发最难的题,第 12 章还会提。
五、动手 Demo:CPU 同时运行多个线程
两种任务各跑一遍:一遍单线程做 4 份,一遍 4 线程各做 1 份。 看时间差多少。
import threading, time
def burn(n):
s = 0
for i in range(n):
s += i
# 算数:单线程 4 份 vs 4 线程各 1 份
# 等待:time.sleep(0.2) 各 4 次,同样两种跑法
实跑输出:
--------------------------------------------------------------------
CPU 同时运行多个线程
--------------------------------------------------------------------
【同时】这个词要看任务类型。**实测两种任务,4 个线程对单线程:**
任务类型 单线程跑 4 份 4 线程各跑 1 份 快了吗
-----------------------------------------------------------------------
算数(吃 CPU) 0.032 秒 0.029 秒 几乎没快
等待(不吃 CPU) 0.802 秒 0.202 秒 快了 4.0 倍
**实测结论很清楚:吃 CPU 的活儿,4 个线程和 1 个线程差不多;等待的活儿快 4 倍。**
原因是 Python 有 **GIL**(全局解释器锁):**同一时刻只有一个线程能执行字节码。**
**所以线程的价值在【等】上,不在【算】上。**这一条对后面讲服务器并发很关键。
实测:算数任务两种跑法都约 0.03 秒(几乎没快),等待任务从 0.8 秒降到 0.2 秒(快 4 倍)。 原因是 GIL:同一时刻只有一个线程执行字节码——线程的价值在【等】上,不在【算】上。
30 秒实验
- 把一段读文件的代码改成多线程,看总时间掉多少。
- 把一段纯计算的代码改成多线程,看它是不是白改。
常见误区
- 以为多线程一定更快。吃 CPU 的活儿不会快。
- 以为线程越多越好。切换本身要花钱,还有共享数据打架。
自测三问(点开对答案)
1. 两类任务的实测结果各是什么? —— 算数两种跑法都约 0.03 秒(几乎没快);等待从 0.8 秒降到 0.2 秒(快 4 倍)。 2. 为什么算数任务多线程不快? —— GIL 让同一时刻只有一个线程执行字节码,多线程只是轮着跑,总时间不变。 3. 什么时候用线程、什么时候用进程? —— IO 密集用线程(等的时候能重叠),CPU 密集用多进程(绕开 GIL 真并行)。- ☐ 下一节(5.5 使用静态区、堆区和栈区进行管理)
相关笔记
- 使用静态区、堆区和栈区进行管理 — 线程各有一份栈
- 进化的OOP结构之三:垃圾回收 — 堆上的对象谁回收
- 🖥️ 服务器工作原理 · 索引 — 并发与线程模型
- 🧩 面向对象是怎样工作的 · 索引