本文翻译自我的英文博客,最新修订内容可随时参考:CPU can only see the threads
在 Python 中,由于 GIL(全局解释器锁)的存在——这是一个互斥锁,确保同一时刻只有一个线程能执行——因此在 CPython 解释器下不支持多线程并行执行。但多进程呢?它们之间的区别是什么?如何选择合适的方法?你了解协程吗?让我们一起探讨。
首先,你需要了解以下基本概念:
对于每个进程,实际的执行单元是进程中的主线程。因此,即使在不同进程中,CPU 实际看到的仍然是线程。
计算机的核心是可同时并行的物理核心数量(CPU 只能“看到”线程)。由于超线程技术(Hyper-Threading),实际可并行的线程数通常是物理核心数的 两倍(这也是操作系统看到的核心数)。我们只关心可并行的线程数,因此 后文提到的核心数均指操作系统看到的核心数(即包含超线程后的逻辑核心,非物理核心)。
缺点:
若 CPU 满负载,合理的时间分配应为:
其他语言中,多线程可并行运行在多个核心上,但在 Python 中,同一时刻只有一个线程能获取 CPU。
Python 的 GIL(全局解释器锁)确保同一时刻只有一个线程执行字节码。Python 有多种解释器:
为什么只有 CPython 有 GIL?
CPython 的内存管理(如垃圾回收)并非线程安全。若无 GIL,多线程同时操作对象时可能导致内存泄漏或程序崩溃。GIL 确保同一时刻只有一个线程访问 CPython 解释器,从而保证线程级安全。
线程安全主要涉及内存安全。进程内所有线程共享堆内存,若无保护机制,数据可能被其他线程意外修改。常见解决方案包括互斥锁(Mutex)、信号量(Semaphore)等。
# 直接使用 threading 模块
import threading
def run(n):
print("当前任务:", n)
if __name__ == "__main__":
t1 = threading.Thread(target=run, args=("线程 1",))
t2 = threading.Thread(target=run, args=("线程 2",))
t1.start()
t2.start()
# 自定义线程类
class MyThread(threading.Thread):
def __init__(self, name):
super().__init__()
self.name = name
def run(self):
print("当前任务:", self.name)
# 主线程等待子线程完成
t1.start()
t2.start()
t1.join() # 阻塞主线程,直至 t1 完成
t2.join(timeout=5) # 带超时的等待
# 守护线程(Daemon Thread):随主线程退出而终止
t1.setDaemon(True)
t1.start()
# 定时器:延迟执行任务
t = threading.Timer(1, show) # 1 秒后执行 show 函数
t.start()
# 线程局部存储(每个线程独立数据)
local_school = threading.local() # 创建线程局部变量
local_school.student = "Alice" # 仅当前线程可见
# 互斥锁(Lock)
mutex = threading.Lock()
mutex.acquire() # 加锁
# 临界区代码
mutex.release() # 解锁
# 可重入锁(RLock):允许同一线程多次获取锁
reentrant_lock = threading.RLock()
# 信号量(Semaphore):限制同时访问资源的线程数
semaphore = threading.BoundedSemaphore(5) # 最多 5 个线程同时获取锁
多进程可绕过 GIL 限制,充分利用多核 CPU。
# 基本用法
from multiprocessing import Process
def show(name):
print("子进程名称:", name)
if __name__ == "__main__":
proc = Process(target=show, args=("子进程",))
proc.start()
proc.join() # 等待子进程完成
协程是用户态的轻量级线程,基于事件循环(Event Loop)和 await
主动切换,无需操作系统调度。
await
切换)。 import asyncio
balance = 0
async def update_balance(n):
global balance
balance += n
await asyncio.sleep(1) # 主动让出控制权(模拟 IO 阻塞)
balance -= n
print(balance)
# 运行协程
loop = asyncio.get_event_loop()
loop.run_until_complete(asyncio.gather(
update_balance(10),
update_balance(20)
))
print("最终余额:", balance) # 输出:0
注意:
await
,则顺序执行,结果确定。 await
,需通过锁(如 asyncio.Lock
)保证数据一致性,此时异步会退化为同步。 场景 | 推荐方案 | 理由 |
---|---|---|
CPU 密集型任务 | 多进程 | 规避 GIL,利用多核并行 |
IO 密集型任务 | 多线程/协程 | 线程在 IO 阻塞时释放 GIL,协程轻量级适合高并发 IO |
超高频调度任务 | 协程 | 无内核上下文切换,调度成本极低 |
跨机器通信 | 多进程+套接字 | 进程隔离性强,套接字支持分布式通信 |
参考资料:
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。