为了账号安全,请及时绑定邮箱和手机立即绑定

Python中的并发编程:详解多线程、多进程与异步(asyncio)

标签:
Python
目录
  1. TL;DR
  2. 介绍
  3. 线程与进程
  4. 多线程与多进程
  5. Asyncio
  6. 常见误区和错误
  7. 何时使用每种方法
  8. FastAPI异步编程示例
  9. 总结
概要

参考这个: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():让主线程等待该线程结束。
线程 vs 进程
多线程

多线程允许多个线程在同一进程中并发运行。在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

Asyncio 是一个 Python 库,用于使用 async/await 语法编写并发代码。它专为 I/O 密集的任务设计,并使用事件循环来管理和调度任务,利用事件循环。

异步IO中的几个关键概念
  1. 协程:用 async def 定义的函数。这些都是 asyncio 的构建块,可以被暂停和恢复。
  2. 事件循环:asyncio 的核心,管理任务的执行。
  3. 任务:这是协程的包装器,它们被安排在事件循环上执行。
  4. **await**:暂停协程,将执行权交回事件循环。

示例:Asyncio 基础知识
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():并行运行多个协程。
异步IO编程中的CPU密集型任务处理

Asyncio 不太适合处理 CPU 绑定任务,因为这会阻塞事件循环。然而,你可以使用 asyncio.to_thread()asyncio.run_in_executor() 将这些任务转移到单独的线程或进程中。

一个例子:把一个CPU密集任务转交给其他组件
    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():当你想启动一个协程的同时继续做其他工作时非常有用。

当何时使用每种方法更为合适
  1. 多线程:
    - 最适合IO密集型任务,如网络操作或文件IO。
    - 当你需要在各个线程之间共享状态时使用。
    - 不适合CPU密集型任务,因为Python中的全局解释器锁(GIL)会限制并行性。
  2. 多进程:
    - 适合需要真正并行性的CPU密集型任务。
    - 当你需要绕过GIL时使用。
    - 最适合高计算量的工作负载。
  3. 异步IO:
    - 适合具有大量并发操作的IO密集型任务。
    - 适合构建高性能的网络服务器或具有大量IO密集型任务的程序。
    - 不适合无法卸载到其他线程的CPU密集型任务。

FastAPI中的异步处理示例

FastAPI 是一个现代的 Web 框架,利用 asyncio 高效处理并发请求。它使用 async/await 语法来管理 I/O 操作,这样就不会阻塞服务器。

为什么 FastAPI 使用异步
  1. 可扩展性:异步代码让FastAPI能够以最小的开销处理许多并发连接。
  2. 性能:对于I/O密集型任务,在处理上异步可以胜过传统的线程。
  3. 简洁性:与线程代码相比,异步代码通常更简单易懂。
在FastAPI中处理CPU绑定任务

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应用程序的性能和扩展性。

点击查看更多内容
TA 点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
  • 推荐
  • 评论
  • 收藏
  • 共同学习,写下你的评论
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消