Python 线程与协程
选择模型
本篇是语言基础后的并发实践。先掌握 函数与模块、异常清理 与 标准库文件操作,再按实际 I/O 任务选择模型。
并发表示任务的执行过程交叠,并行表示实际同时执行。下面两种模型都常用于 I/O 等待,但不保证 CPU 计算提速。
- 已有阻塞 I/O 库可考虑线程;共享可变状态需要锁或队列协调。
- 库提供异步接口时可使用
asyncio;协程在await处让出执行权,阻塞调用会卡住事件循环。 - 默认带 GIL 的 CPython 中,CPU 密集的 Python 字节码通常不能靠多线程并行加速;可评估多进程。自由线程构建的行为有差异,不能把 GIL 当作所有版本、构建的固定属性。见 threading 文档。
线程:启动与等待
start() 启动线程,join() 等待结束。输出先后不能作为线程调度保证;后台线程里的异常不会自动在主线程以原异常重新抛出。
import threading
def show(label):
print(label)
threads = [threading.Thread(target=show, args=(label,)) for label in ["A", "B"]]
for thread in threads:
thread.start()
for thread in threads:
thread.join()
assert all(not thread.is_alive() for thread in threads)多进程有独立地址空间,但可以通过 IPC、共享内存等显式通信;“完全不会共享资源”不准确。
协程:等待与并发
调用 async def 函数产生协程对象;仅创建对象不会执行任务。用 asyncio.run 启动,gather 并发等待独立协程并按传入顺序返回结果。
import asyncio
async def wait_and_return(label, delay):
await asyncio.sleep(delay)
return label
async def main():
values = await asyncio.gather(
wait_and_return("A", 0.01),
wait_and_return("B", 0.02),
)
assert values == ["A", "B"]
if __name__ == "__main__":
asyncio.run(main())两次等待交叠,总等待接近较长的那次加调度开销,不是两次延迟相加。await 不会自动把同步阻塞函数变成异步函数;在已有事件循环的环境中也不能再直接嵌套 asyncio.run()。gather 默认传播首个异常,其他任务不会因此自动全部取消,真实业务要设计取消与清理。
练习与资料
比较顺序 await 与 gather 两种方式,但不要用严格墙钟耗时断言判断正确性。让一个任务失败,观察异常传播并补清理逻辑。参见 asyncio 任务文档。
练习核对
这里的顺序await指直接依次等待两个尚未启动的协程,例如在async函数中先写 a = await wait_and_return("A", 0.01),再写 b = await wait_and_return("B", 0.02);只有A结束后才调用并推进B。若先用 asyncio.create_task 调度A、B,再依次 await task_a、await task_b,任务仍可交叠,等待句柄的顺序不等于启动顺序。gather也使两段等待交叠,但返回仍按A、B的参数顺序。提示:正确性核对返回值和清理标志,不断言必须小于某毫秒。让A在await后抛ValueError时,普通gather会把该异常交给调用方,B不会仅因gather失败就自动取消;若main因此结束,asyncio.run关闭循环时又会取消剩余任务,要区分这两个来源。任务内部用try/finally记录清理,调用方显式取消并await仍未完成的任务;结构化实现见TaskGroup流水线。本轮未运行线程或事件循环。
继续阅读
- 知识专题
- 排错
- 排错