python中的多线程和协程
在 Python 中,多线程与协程都是实现并发编程的常用手段,但它们的实现机制、资源消耗和适用场景各不相同。下面将详细说明二者的原理、区别及各自的优缺点。
1. 多线程
原理与实现
- 多线程模型
多线程是在一个进程内部创建多个线程,每个线程拥有自己的调用栈和执行上下文。Python 提供了标准库 threading 来实现多线程。 - 全局解释器锁(GIL)
在 CPython 解释器中,为保证内存管理的线程安全,同一时刻只允许一个线程执行 Python 字节码,这就是全局解释器锁(GIL)。因此,多线程在 CPU 密集型任务中往往无法实现真正的并行,只能在 I/O 阻塞时切换【citeturn1search0】。 - 适用场景
多线程适用于 I/O 密集型任务,例如网络请求、文件读写等。即使存在 GIL,当线程因为等待 I/O 操作而阻塞时,其他线程就可以获得执行机会,从而提升整体效率【citeturn1search3】。
优点与缺点
- 优点:
- 适合处理 I/O 阻塞任务,在等待 I/O 时可切换到其他线程。
- 能利用操作系统的线程调度机制,代码实现简单(例如使用
threading.Thread创建线程)。
- 缺点:
- 由于 GIL 的存在,在 CPU 密集型任务中无法真正利用多核优势;线程切换也会带来一定的开销。
- 线程之间共享内存,容易引发数据竞争问题,需要借助锁机制(如
threading.Lock)来确保数据一致性,增加了编程复杂度【citeturn1search5】。
示例
下面是一个使用多线程执行 I/O 模拟任务的简单示例:
import threading
import time
def worker(name):
print(f"{name} 开始工作")
time.sleep(2) # 模拟 I/O 阻塞
print(f"{name} 工作完成")
threads = []
for i in range(5):
t = threading.Thread(target=worker, args=(f"线程{i+1}",))
t.start()
threads.append(t)
for t in threads:
t.join()
print("所有线程工作完成")
线程创建与执行流程分析
1. 线程对象创建
t = threading.Thread(target=worker, args=(f"线程{i+1}",))
- 实例化 Thread 类创建线程对象
t target=worker:指定线程要执行的函数是workerargs=(f"线程{i+1}",):传递参数(元组形式),为每个线程分配唯一名称(如"线程1"、"线程2"等)
2. 线程启动与跟踪
t.start()
threads.append(t)
t.start():调用线程对象的start()方法,启动线程(开始执行worker函数)threads.append(t):将启动的线程对象添加到threads列表中,便于后续跟踪
3. 线程同步控制
for t in threads:
t.join()
- 遍历
threads列表中存储的所有线程对象 t.join():调用线程对象的join()方法,让主线程等待当前子线程执行完毕- 关键作用:确保"所有线程工作完成"的提示在最后输出
关键特性:
start()立即触发线程异步执行(非阻塞)join()实现线程同步(阻塞主线程)- 所有线程并行执行(时间重叠)
- 主线程最后输出完成提示
在Python多线程编程中,start()和join()是两个核心方法,它们的区别和执行结果如下:
1. start() 方法
- 作用:启动线程,使其进入就绪状态
- 行为:
- 调用后线程开始执行
target函数 - 立即返回,不阻塞主线程
- 实际执行时间由操作系统调度器决定
- 调用后线程开始执行
- 特点:每个线程只能调用一次
start()
2. join() 方法
- 作用:阻塞调用线程,直到目标线程完成
- 行为:
- 使主线程暂停执行,等待目标线程结束
- 如果线程已结束,则立即返回
- 特点:用于线程同步,确保线程执行顺序
3. 程序执行结果分析
线程1 开始工作
线程2 开始工作
线程3 开始工作
线程4 开始工作
线程5 开始工作
# (约2秒后同时出现,顺序随机)
线程3 工作完成
线程1 工作完成
线程4 工作完成
线程2 工作完成
线程5 工作完成
所有线程工作完成
4. 关键区别总结
| 特性 | start() |
join() |
|---|---|---|
| 阻塞性 | 非阻塞 | 阻塞主线程 |
| 调用时机 | 线程创建后立即调用 | 需要等待线程结束时调用 |
| 作用对象 | 启动新线程 | 同步已启动的线程 |
| 执行位置 | 主线程中调用 | 主线程中调用 |
| 主要目的 | 启动并发执行 | 确保线程完成 |
5. 执行流程说明
- 主线程快速创建并启动5个工作线程
- 所有工作线程几乎同时开始执行(打印"开始工作")
- 所有线程同时进入2秒休眠(模拟I/O阻塞)
- 主线程在
join()循环中被阻塞,等待所有线程 - 各线程随机顺序唤醒并完成工作
- 当所有线程结束后,主线程继续执行最后打印
⚠️ 注意:由于GIL(全局解释器锁)的存在,Python线程更适合I/O密集型任务(如本例),对CPU密集型任务建议使用
multiprocessing模块。
在这个例子中,虽然每个线程会阻塞 2 秒,但由于多个线程可以并发执行,总耗时远低于串行执行的时间。
2. 协程
原理与实现
- 协程模型
协程是一种轻量级的并发执行方式,在一个线程内通过协作式调度(也称为“协作多任务”或“非抢占式多任务”)实现多个任务间的切换。Python 3 中使用asyncio模块结合async/await语法来实现协程。 - 调度机制
协程的切换由程序员显式控制(例如在await语句处让出控制权),切换开销非常小,而且内存消耗低。由于协程在单线程内执行,完全不会涉及操作系统级别的线程上下文切换【citeturn1search7】。 - 适用场景
协程非常适合于 I/O 密集型任务,尤其是大量网络请求、数据库查询、文件操作等场景。因为在这些任务中,程序大部分时间处于等待状态,协程能够高效地利用这部分时间并调度其他任务【citeturn1search1】。
优点与缺点
- 优点:
- 切换开销极小,内存占用低。
- 编程模型简洁,避免了线程间的锁竞争问题(所有协程都运行在单线程中)。
- 对于大量 I/O 阻塞任务能极大提升并发性能。
- 缺点:
- 协程本质上是单线程的,无法利用多核 CPU 的并行计算能力,因此不适合 CPU 密集型任务。
- 需要配合事件循环(如
asyncio.run())来管理调度,对于初学者来说理解和调试可能稍显复杂【citeturn1search8】。
示例
下面是一个使用 asyncio 进行异步网络请求的示例:
import asyncio
import aiohttp
async def fetch_url(url):
async with aiohttp.ClientSession() as session:
async with session.get(url) as response:
return await response.text()
async def main():
urls = [
'https://www.example.com',
'https://www.google.com',
'https://www.openai.com'
]
tasks = [asyncio.create_task(fetch_url(url)) for url in urls]
responses = await asyncio.gather(*tasks)
for response in responses:
print(response[:100]) # 打印每个响应的前100个字符
asyncio.run(main())
这个示例中,多个网络请求通过协程并发执行,整体运行效率远高于串行方式。
在Python协程中,yield和await都是主动让出控制权的关键字,但它们在语义和使用场景上有重要区别:
核心区别总结
| 特性 | yield |
await |
|---|---|---|
| 定位 | 生成器核心机制 | 异步编程核心机制 |
| 主要用途 | 生成器值传递/暂停 | 等待异步操作完成 |
| 调度方式 | 手动调度(需主动调用next()) |
自动调度(由事件循环驱动) |
| 返回值 | 向调用方传递数据 | 获取异步操作结果 |
| 上下文 | 可在普通函数使用 | 必须在async def函数内使用 |
详细解析
1. yield(生成器核心)
def generator():
print("Start")
data = yield "Ready" # ① 让出控制权并返回值
print(f"Received: {data}") # ② 从send()接收值
yield "Done"
gen = generator()
print(next(gen)) # 输出: Start → Ready (首次激活)
print(gen.send(123)) # 输出: Received: 123 → Done
- 让出控制权:暂停执行并返回
yield右侧的值 - 双向通信:通过
send()向生成器注入数据 - 手动驱动:需显式调用
next()或send()恢复执行
2. await(异步编程核心)
import asyncio
async def async_task():
print("Start task")
result = await asyncio.sleep(1, "Result") # ① 让出控制权
print(f"Got: {result}") # ② 自动获取异步结果
async def main():
await asyncio.gather(async_task(), async_task()) # 并发执行
asyncio.run(main())
- 让出控制权:暂停当前协程,将控制权交还事件循环
- 自动恢复:当等待的操作(如I/O)完成后自动唤醒
- 结果传递:直接返回异步操作的结果(如
asyncio.sleep返回的"Result")
关键差异点
-
调度机制
yield:需要外部调用者手动驱动(生成器协议)await:由事件循环自动调度,对开发者透明
-
使用场景
yield:构建生成器、传统协程(Python 3.4前)await:现代异步I/O操作(网络请求、文件读写等)
-
错误处理
yield:通过throw()方法注入异常await:通过try/except捕获异步异常
-
语法限制
yield可在普通函数使用await必须存在于async def定义的协程函数中
💡 历史演进:Python 3.4的
@asyncio.coroutine使用yield from实现协程,而Python 3.5+的async/await是更简洁的替代方案,具有更好的性能和可读性。
一句话总结
yield是生成器的暂停/恢复机制,await是异步操作的等待/唤醒机制。两者都让出控制权,但await专为异步编程设计,与事件循环深度集成,能自动处理I/O等待和任务调度。
3. 多线程与协程的对比
并发模型与调度
-
多线程:
- 由操作系统调度,线程切换需要保存/恢复线程状态,开销较大。
- 每个线程拥有独立调用栈,系统资源占用较高。
- 受 GIL 限制,无法在 CPU 密集型任务中实现真正的并行执行【citeturn1search0】。
-
协程:
- 由用户态调度,任务主动让出控制权,切换开销极小。
- 所有协程共享同一线程环境,无需分配额外的线程上下文。
- 适合 I/O 密集型任务,但无法利用多核并行计算【citeturn1search7】。
适用场景
- 使用多线程时:
- 任务中既有 I/O 操作也有一定的计算(但计算量不高)。
- 希望利用操作系统线程调度机制,同时兼顾部分并行计算(尤其在非 CPython 解释器中,如 Jython、IronPython)。
- 使用协程时:
- 任务主要为 I/O 阻塞操作,如网络请求、文件读写、数据库查询等。
- 需要处理大量并发连接且对资源消耗要求较高时,协程可以大幅提升性能。
此外,在一些实际场景中,还可以结合使用多进程、多线程与协程,例如利用多进程突破 GIL 限制,再在每个进程内使用协程处理 I/O 密集型任务【citeturn1search4】。
4. 总结
- 多线程适用于 I/O 密集型任务,但受限于 GIL,在 CPU 密集型任务中可能无法达到理想的并行效果。线程切换带来的开销和资源消耗也较高。
- 协程通过非阻塞的方式在单线程中调度多个任务,切换开销小、资源占用低,非常适合处理大量 I/O 操作,但不能利用多核并行计算。
- 选择哪种模型应根据任务的具体特点、性能需求以及开发者对并发模型的熟悉程度来决定;在复杂场景下,多种模型结合使用往往能取得更佳效果【citeturn1search5】【citeturn1search7】。
通过理解多线程与协程的区别,可以更好地设计并发程序,充分利用 Python 提供的并发工具来提高程序的响应性和运行效率。

浙公网安备 33010602011771号