当前位置:首页>python>用了三年 Python 多线程,才发现我一直在用假并发

用了三年 Python 多线程,才发现我一直在用假并发

  • 2026-10-11 06:07:06
用了三年 Python 多线程,才发现我一直在用假并发

三年前我刚入行,接手了一个爬虫项目,跑得奇慢。

前辈说:"加多线程,快多了。"

我照着文档写了 threading.Thread,开了十个线程并发抓页面,速度确实蹭蹭往上走,心里暗爽——多线程果然是提速神器。

后来有个计算密集型任务,矩阵乘法,我的第一反应:加线程!

然后我发现——加了和没加一个速度,甚至慢了一点点。

那一刻我陷入了困惑。


先把这个问题搞清楚

import threading
import time

def cpu_task(n):
    """纯 CPU 计算"""
    total = 0
    for i in range(n):
        total += i * i
    return total

# 单线程跑两次
start = time.time()
cpu_task(10_000_000)
cpu_task(10_000_000)
single_time = time.time() - start
print(f"单线程耗时: {single_time:.3f}s")

# 双线程并行跑
start = time.time()
t1 = threading.Thread(target=cpu_task, args=(10_000_000,))
t2 = threading.Thread(target=cpu_task, args=(10_000_000,))
t1.start()
t2.start()
t1.join()
t2.join()
thread_time = time.time() - start
print(f"双线程耗时: {thread_time:.3f}s")

print(f"多线程是单线程的 {single_time/thread_time:.2f} 倍速")

输出(真实测试结果,Python 3.11,4 核机器):

单线程耗时: 2.841s
双线程耗时: 3.127s
多线程是单线程的 0.91 倍速

没有快,反而慢了。

这不是玄学,这就是 GIL 的本质。


GIL 是什么

全局解释器锁,Global Interpreter Lock。

Python 的解释器(CPython)内部有个锁,同一时刻只允许一个线程执行 Python 字节码。你开十个线程,表面上是"并发",底层是这十个线程在轮流争同一把锁。

对于 CPU 密集型任务,多线程没有任何提速效果,还因为线程切换的开销略微变慢。

那爬虫为什么快了?

因为爬虫是 IO 密集型——大量时间花在等待网络响应上。等待期间线程主动释放 GIL,其他线程可以趁机运行。这时候多线程确实有用,不是因为"并行计算",而是因为"等待时间重叠"。

本质上是时间的重叠,不是 CPU 资源的并行利用。


对比一下真正的并行

用 multiprocessing 绕过 GIL,每个进程有独立的 Python 解释器,真正利用多核:

import multiprocessing
import threading
import time

def cpu_task(n):
    total = 0
    for i in range(n):
        total += i * i
    return total

N = 10_000_000

# 单线程基准
start = time.time()
cpu_task(N)
cpu_task(N)
single = time.time() - start

# 多线程(受 GIL 限制)
start = time.time()
t1 = threading.Thread(target=cpu_task, args=(N,))
t2 = threading.Thread(target=cpu_task, args=(N,))
t1.start(); t2.start()
t1.join(); t2.join()
thread = time.time() - start

# 多进程(真并行)
start = time.time()
with multiprocessing.Pool(2) as pool:
    pool.map(cpu_task, [N, N])
multi = time.time() - start

print(f"单线程:   {single:.3f}s  (基准)")
print(f"多线程:   {thread:.3f}s  (比单线程 {single/thread:.2f}x)")
print(f"多进程:   {multi:.3f}s   (比单线程 {single/multi:.2f}x)")

输出:

单线程:   2.847s  (基准)
多线程:   3.091s  (比单线程 0.92x)
多进程:   1.523s  (比单线程 1.87x)

多线程几乎没有提速,多进程接近两倍加速(双核理论上限就是 2x,实际损耗一点)。


那多线程到底该用在哪?

做一个简单的场景对照:

import threading
import concurrent.futures
import time
import urllib.request

# 模拟 IO 等待(用 time.sleep 代替网络请求)
def io_task(task_id):
    time.sleep(0.5)  # 模拟 0.5 秒网络延迟
    return f"task-{task_id} done"

tasks = list(range(10))

# 顺序执行
start = time.time()
results = [io_task(t) for t in tasks]
seq_time = time.time() - start

# 多线程并发
start = time.time()
with concurrent.futures.ThreadPoolExecutor(max_workers=10) as executor:
    results = list(executor.map(io_task, tasks))
thread_time = time.time() - start

print(f"顺序执行 10 个 IO 任务: {seq_time:.2f}s")
print(f"多线程执行 10 个 IO 任务: {thread_time:.2f}s")
print(f"加速比: {seq_time/thread_time:.1f}x")

输出:

顺序执行 10 个 IO 任务: 5.01s
多线程执行 10 个 IO 任务: 0.51s
加速比: 9.8x

将近 10 倍,这才是多线程应该发光的地方。


一张表总结清楚

| 场景 | 用什么 | 原因 |

|------|--------|------|

| 网络请求、数据库查询、文件读写 | threading 或 asyncio | IO 等待期间释放 GIL,时间可重叠 |

| 数学计算、数据处理、模型训练 | multiprocessing 或 numpy | 绕过 GIL 或底层 C 实现,真并行 |

| 高并发轻量连接 | asyncio | 单线程事件循环,零线程切换开销 |


一个常见误区

有人会说:用 NumPy 做矩阵运算,多线程也能提速啊?

那是因为 NumPy 的底层是 C/Fortran 实现,计算过程中会主动释放 GIL。不是多线程"破解"了 GIL,而是 NumPy 根本没用 Python 字节码来做真正的计算。

所以这个结论更准确:GIL 限制的是 Python 字节码的并发执行,不是所有 C 扩展。

这也是为什么 Python 在数据科学领域这么好用——真正的计算都外包给 C/Fortran/CUDA,Python 只是个调度层。


最后说一句实在话

很多人(包括三年前的我)把多线程当成"提速魔法",觉得加线程就是加速。

这个认知是错的,而且代价不小——写了一堆锁、线程同步、竞争条件的代码,最后发现 CPU 利用率完全没变。

正确的认知应该是:先判断你的瓶颈在 IO 还是在 CPU,再选工具。

不理解 GIL,就会在错误的方向上用力。

最后,别忘了关注「有为大青年」,我们下期见~

这个坑你踩过吗?欢迎留言聊聊——或者转发给那个还在用多线程跑 CPU 任务的朋友,救他一命~

最新文章

随机文章