- TL;DR
- 介绍
- 线程与进程
- 多线程与多进程
- Asyncio
- 常见误区和错误
- 何时使用每种方法
- FastAPI异步编程示例
- 总结
参考这个:Stack Overflow(Stack Overflow)
简介并发是编程中的一个重要概念,允许应用程序同时执行多个任务。Python 提供了多种管理并发的工具:线程、多进程和异步编程(如 Python 中的 asyncio 模块)。每种工具都有自己独特的优点,适用于不同类型的任务,各有特色。本文深入探讨了这些并发模型,提供了清晰的例子和详细的解释来帮助你理解,何时以及如何有效地运用它们。
线程与进程 过程一个进程是程序执行过程中独立的实例。每个进程都有操作系统为其分配的独立资源。进程一般不会与其他进程共享内存,除非通过进程间通信(IPC)设计为共享。
线程(话题)线程是进程中的最小执行单元。同一进程内的多个线程共享同一内存空间,使得它们比单独的进程更高效地通信。然而,这种共享内存也可能引发同步问题。
示例:在Python中创建一个线头 import threading
import time
def print_numbers():
# 此函数将在一个单独的线程中运行
for i in range(5):
print(f"线程: {i}")
time.sleep(1) # 使用time.sleep()来模拟一些线程任务
# 创建一个新的线程对象来运行print_numbers()
thread = threading.Thread(target=print_numbers)
# 启动这个线程
thread.start()
# 在主程序退出前等待这个线程完成
thread.join()
print("主线程: 执行完毕")
无内容
threading.Thread(target=print_numbers)
:创建一个将执行print_numbers()
函数的线程。thread.start()
:启动线程。thread.join()
:让主线程等待该线程结束。
多线程允许多个线程在同一进程中并发运行。在Python中,由于Python中的全局解释器锁(GIL),多线程的真正并行性受到限制,每次只能有一个线程执行Python字节码。然而,对于I/O密集的任务,多线程依然很有帮助,因为线程可以在等待外部资源(如文件I/O或网络操作)时,其他线程可以继续运行。
示例:Python中的多线程处理 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=(i,))
threads.append(t)
t.start()
for t in threads:
t.join() # 等待所有线程结束
说明:
此处应有内容但未填写
- 每个线程通过休眠2秒钟来模拟I/O绑定的工作。
thread.join()
,确保主线程等待所有工作线程结束。
多进程涉及到运行多个独立的进程,每个进程都有自己的Python解释器和独立的内存空间。这使得真正的并行处理得以实现,因此,多进程非常适合处理器密集型的任务。
示例:Python多进程示例: import multiprocessing
import time
def worker(name):
print(f"工人 {name} 开始工作")
time.sleep(2) # 模拟工作
print(f"工人 {name} 完成工作")
if __name__ == '__main__':
进程们 = []
for i in range(5):
p = multiprocessing.Process(target=worker, args=(i,))
进程们.append(p)
p.start()
for p in 进程们:
p.join() # 等待进程结束
- 每个工作进程都可以独立运行,实现了真正的多核并行处理。
- 多进程避免了GIL的限制,适合CPU密集型计算任务。
Asyncio 是一个 Python 库,用于使用 async/await
语法编写并发代码。它专为 I/O 密集的任务设计,并使用事件循环来管理和调度任务,利用事件循环。
- 协程:用
async def
定义的函数。这些都是 asyncio 的构建块,可以被暂停和恢复。 - 事件循环:asyncio 的核心,管理任务的执行。
- 任务:这是协程的包装器,它们被安排在事件循环上执行。
**await**
:暂停协程,将执行权交回事件循环。
import asyncio
async def task(name):
print(f"任务 {name} 开始了")
await asyncio.sleep(2) # 模拟 I/O 操作的等待
print(f"任务 {name} 结束了")
async def main():
await asyncio.gather(task("A"), task("B"), task("C"))
asyncio.run(main())
解释:
await asyncio.sleep(2)
:暂停协程,让事件循环有机会运行其他任务。asyncio.gather()
:并行运行多个协程。
Asyncio 不太适合处理 CPU 绑定任务,因为这会阻塞事件循环。然而,你可以使用 asyncio.to_thread()
或 asyncio.run_in_executor()
将这些任务转移到单独的线程或进程中。
import asyncio
import time
def cpu_bound_task(n):
time.sleep(n) # 模拟一个CPU密集型操作
return n * n
async def main():
result = await asyncio.to_thread(cpu_bound_task, 2)
print(f"结果是: {result}")
asyncio.run(main())
asyncio.to_thread()
:将 CPU 密集型的任务卸载到一个独立的线程,让事件循环保持响应性。
同步和异步代码的混合:
并非所有操作都需要异步。可以通过 asyncio.to_thread()
或其他类似方式在异步代码中调用同步函数。
示例:
import asyncio
import time
def sync_task():
time.sleep(2)
return "任务完成"
async def main():
result = await asyncio.to_thread(sync_task) # asyncio.to_thread is a library function, keeping it in English
print("结果:", result)
asyncio.run(main())
直接等待CPU密集型任务:
直接等待CPU密集型任务会阻塞事件循环,应该将此类任务移到单独的线程或进程中处理。
**create_task()**
vs **await**
直接:
await coroutine
:运行协程并等待它完成。asyncio.create_task(coroutine)
:让协程异步运行然后立即返回。之后你可以稍后再等待该任务。
比如说:
import asyncio
async def my_coroutine():
await asyncio.sleep(2)
return "Done"
async def 主函数():
任务 = asyncio.create_task(my_coroutine())
print("等待的时候做其他事情...")
结果 = await 任务
print(f"任务的结果是: {结果}")
asyncio.run(主函数())
说明:
asyncio.create_task()
:当你想启动一个协程的同时继续做其他工作时非常有用。
- 多线程:
- 最适合IO密集型任务,如网络操作或文件IO。
- 当你需要在各个线程之间共享状态时使用。
- 不适合CPU密集型任务,因为Python中的全局解释器锁(GIL)会限制并行性。 - 多进程:
- 适合需要真正并行性的CPU密集型任务。
- 当你需要绕过GIL时使用。
- 最适合高计算量的工作负载。 - 异步IO:
- 适合具有大量并发操作的IO密集型任务。
- 适合构建高性能的网络服务器或具有大量IO密集型任务的程序。
- 不适合无法卸载到其他线程的CPU密集型任务。
FastAPI中的异步处理示例
FastAPI 是一个现代的 Web 框架,利用 asyncio 高效处理并发请求。它使用 async/await 语法来管理 I/O 操作,这样就不会阻塞服务器。
为什么 FastAPI 使用异步- 可扩展性:异步代码让FastAPI能够以最小的开销处理许多并发连接。
- 性能:对于I/O密集型任务,在处理上异步可以胜过传统的线程。
- 简洁性:与线程代码相比,异步代码通常更简单易懂。
FastAPI可以将其转移到线程池或进程池来处理CPU绑定的任务。
示例:在FastAPI中处理CPU占用型任务 from fastapi import FastAPI
from concurrent.futures import ProcessPoolExecutor
import asyncio
app = FastAPI()
process_pool = ProcessPoolExecutor()
def cpu_bound_task(n):
# 模拟一个CPU密集型的任务
total = 0
for i in range(n):
total += i * i
return total
@app.get("/compute/{n}")
async def compute(n: int):
# 将CPU密集型任务移到单独的进程中
loop = asyncio.get_running_loop()
result = await loop.run_in_executor(process_pool, cpu_bound_task, n)
return {"result": result}
说明:
- ProcessPoolExecutor:我们创建了一个
ProcessPoolExecutor
,将 CPU 密集型工作卸载到一个单独的进程中。这保证了主 FastAPI 事件循环的响应性。其实现由 Uvicorn 内部处理。 **loop.run_in_executor()**
:此方法将cpu_bound_task
卸载至执行器(在这种情况下是ProcessPoolExecutor
),允许 FastAPI 服务器在处理 CPU 密集型工作的同时继续处理其他请求。**await**
:通过使用await
,我们确保 FastAPI 处理程序在返回结果之前等待 CPU 密集型工作的完成。
在 web 应用里,响应速度很重要。如果你直接在 FastAPI 事件循环中运行 CPU 密集型任务,这会让服务器卡住,无法处理其他请求,直到任务完成。通过将任务移到单独的进程或线程中,服务器可以继续处理传入的请求,从而提高扩展性和用户体验。
结论:Python中的并发是一个强大的功能,让你能够编写高效且可扩展的应用程序。无论是处理I/O密集型任务、CPU密集型计算,还是两者都有,Python提供了多种并发模型选择来满足你的需求。多线程、多进程以及asyncio都是可供选择的模型。
- 多线程:最适合需要共享内存的I/O密集型任务场景,但对于CPU密集型任务来说,多线程并不是最佳选择,因为存在GIL的限制。
- 多进程:非常适合需要真正并行处理的CPU密集型任务,避免了GIL的限制。
- Asyncio:非常适合涉及大量并发操作的I/O密集型任务场景,提供了非阻塞的并发处理方式。
使用像FastAPI这样的框架时,理解何时以及如何分发任务是至关重要的。这样做可以帮助你保持应用程序的响应性和扩展性。为具体应用场景选择合适的并发模型,可以大幅提高Python应用程序的性能和扩展性。