Python异步编程(协程)、线程、进程简介

仅供个人学习记录,来源豆包ai

一、Python 异步编程(基于async/awaitasyncio库)

1. 概念

异步编程是一种让程序在遇到 I/O 密集型操作(如网络请求、文件读取等)时,不阻塞当前执行流程,而是可以继续去执行其他不依赖该 I/O 结果的任务,待 I/O 操作完成后再回来处理后续相关事务的编程方式。在 Python 中,通过async def定义异步函数,使用await关键字来暂停异步函数等待可等待对象(如其他异步函数返回的协程对象或特定异步操作对应的对象)完成,而asyncio库的事件循环则负责调度和执行这些异步任务。

2. 特点
  • 高效利用等待时间:在执行多个 I/O 操作时,不会像同步编程那样依次等待每个操作完成,而是并发地处理它们(利用等待 I/O 的空闲时间去执行其他任务),提高了程序整体的运行效率,尤其适合大量 I/O 密集型任务的场景。
  • 轻量级协作:基于协程实现,协程是一种比线程更轻量级的存在,创建和切换协程的开销相对较小,多个协程之间通过await关键字进行协作式的执行切换,不需要像线程那样依赖操作系统内核来进行调度和切换(线程切换涉及到内核态和用户态的切换等,开销相对较大)。
  • 单线程执行:虽然可以实现类似并发的效果,但本质上是在单线程内通过事件循环不断地调度协程来完成多个任务,不存在多线程编程中常见的线程安全问题(如共享资源的竞争、死锁等),不过需要开发者合理地使用await来确保任务的正确执行顺序和依赖关系。
  • 3. 示例应用场景(网络爬虫)
    import asyncio
    import aiohttp
    
    async def fetch(session, url):
        async with session.get(url) as response:
            return await response.text()
    
    async def main():
        async with aiohttp.ClientSession() as session:
            urls = ["http://example.com", "http://python.org", "http://www.baidu.com"]
            tasks = [fetch(session, url) for url in urls]
            results = await asyncio.gather(*tasks)
            for result in results:
                print(result[:100])  # 打印部分内容示例
    
    asyncio.run(main())

    在这个网络爬虫示例中,通过异步编程可以同时发起多个网页的请求,各个请求任务(协程)在等待服务器响应(I/O 操作)的过程中,事件循环会调度其他协程继续执行,等所有请求都有了结果后再统一处理,大大提高了爬取网页的效率。

    二、Python 线程(基于threading模块)

    1. 概念

    线程是操作系统能够进行运算调度的最小单位,它被包含在进程之中,是进程中的实际运作单位。在 Python 中,通过threading模块来创建和管理线程。一个进程可以包含多个线程,这些线程共享进程的资源(如内存空间、文件描述符等),它们可以并发地执行不同的任务,操作系统会根据一定的调度算法来切换线程的执行,让每个线程都有机会使用 CPU 资源。

    2. 特点
  • 共享资源:线程间共享进程的大部分资源,这使得线程之间的数据通信和共享变得相对容易,可以方便地通过共享变量等方式来协同工作,但同时也带来了线程安全问题,例如多个线程同时修改同一个共享变量时,如果没有合适的同步机制(如锁),就可能导致数据不一致的情况。
  • 开销相对较小:相较于进程,线程的创建和销毁开销较小,因为线程是在同一个进程内创建的,不需要像创建进程那样重新分配大量系统资源(如内存空间等),所以在需要频繁创建和销毁执行单元的场景下,线程比进程更有优势。
  • 受限于 GIL(全局解释器锁):在 Python 的标准实现(CPython)中,存在全局解释器锁(GIL),它限制了同一时刻只有一个线程能够执行 Python 字节码,这意味着即使在多核 CPU 的环境下,多线程程序并不能真正地并行执行 Python 代码(不过对于 I/O 密集型任务,线程依然可以通过并发执行,在等待 I/O 时切换线程来提高效率),而是在单核上进行并发切换执行,影响了多线程在计算密集型任务上的性能提升。
  • 3. 示例应用场景(简单的多任务示例)
    import threading
    import time
    
    def print_numbers():
        for i in range(1, 6):
            print(f"线程1: {i}")
            time.sleep(1)
    
    def print_letters():
        for letter in ['a', 'b', 'c', 'd', 'e']:
            print(f"线程2: {letter}")
            time.sleep(1)
    
    thread1 = threading.Thread(target=print_numbers)
    thread2 = threading.Thread(target=print_letters)
    
    thread1.start()
    thread2.start()
    
    thread1.join()
    thread2.join()

    在这个示例中,创建了两个线程分别执行不同的打印任务,它们可以并发地运行,在等待time.sleep(模拟 I/O 操作)的时间里,操作系统会切换线程执行,从而实现两个任务看似 “同时” 进行的效果。

    三、Python 进程(基于multiprocessing模块)

    1. 概念

    进程是计算机中正在运行的程序的实例,它是操作系统进行资源分配和调度的基本单位。每个进程都有自己独立的内存空间、文件描述符等资源,进程之间相互独立,数据不共享(可以通过特定的进程间通信机制来交换数据)。在 Python 中,使用multiprocessing模块来创建和管理多个进程。

    2. 特点
  • 资源独立性:每个进程都拥有独立的资源,这使得进程之间相对独立,一个进程的崩溃通常不会影响到其他进程的正常运行,提高了程序的稳定性和容错性。同时,由于资源独立,不存在像线程那样的共享资源导致的线程安全问题,但相应地,进程间通信(IPC)会比线程间通信复杂一些,需要借助特定的机制(如管道、队列、共享内存等)来实现数据交换。
  • 真正并行执行:与受 GIL 限制的多线程不同,多进程可以充分利用多核 CPU 的优势,在多核环境下多个进程可以真正地并行运行,所以对于计算密集型任务来说,使用多进程往往能获得更好的性能提升,每个进程可以独立地使用一个 CPU 核心来执行任务。
  • 创建销毁开销大:创建一个进程需要操作系统分配独立的内存空间、初始化各种系统资源等,相比线程,其创建和销毁的开销要大得多,所以不适用于需要频繁创建和销毁执行单元的场景。
  • 3. 示例应用场景(利用多进程计算密集型任务)
    import multiprocessing
    import time
    
    def square_numbers(numbers):
        result = []
        for num in numbers:
            result.append(num ** 2)
        return result
    
    if __name__ == "__main__":
        numbers = list(range(10))
        processes = []
        num_processes = multiprocessing.cpu_count()  # 根据CPU核心数创建进程
        chunk_size = len(numbers) // num_processes
    
        for i in range(num_processes):
            start = i * chunk_size
            end = (i + 1) * chunk_size if i < num_processes - 1 else len(numbers)
            p = multiprocessing.Process(target=square_numbers, args=([numbers[start:end]],))
            processes.append(p)
            p.start()
    
        for p in processes:
            p.join()

    在这个示例中,将一个计算列表中数字平方的任务分配到多个进程中去执行,每个进程处理一部分数据,通过利用多核 CPU 实现并行计算,提高了计算密集型任务的执行效率。

    四、三者对比

    1. 资源开销
  • 异步编程(协程):是最轻量级的,创建和切换协程的开销非常小,不需要像线程和进程那样涉及大量系统资源的分配和管理,只是在单线程内基于事件循环调度协程执行。
  • 线程:创建和切换线程的开销相对较小,因为线程共享进程资源,但由于涉及到操作系统的线程调度以及在 Python 中受 GIL 影响,存在一定的资源占用和性能限制情况。
  • 进程:创建和销毁进程的开销最大,需要操作系统分配独立的内存空间等众多资源,但每个进程独立性强,能真正并行执行(多核环境下)。
  • 2. 并发能力
  • 异步编程(协程):通过事件循环调度协程,可以高效并发处理 I/O 密集型任务,实现类似多线程并发的效果,但本质是单线程内的协作式并发,不存在线程安全问题(只要合理使用await等机制)。
  • 线程:可以并发执行多个任务,在 I/O 密集型任务场景下能通过线程切换提高效率,但受 GIL 限制,在计算密集型任务上不能充分利用多核优势实现真正并行。
  • 进程:在多核环境下能真正并行执行多个任务,无论是 I/O 密集型还是计算密集型任务,都可以通过多进程并行来提升效率,但进程间通信相对复杂。
  • 3. 适用场景
  • 异步编程(协程):非常适合大量 I/O 密集型任务的场景,如网络爬虫、Web 应用开发中处理多个客户端请求等,能充分利用等待 I/O 的空闲时间提高效率,同时避免了多线程的线程安全问题和多进程的资源开销大的问题。
  • 线程:适用于 I/O 密集型任务且对资源开销有一定要求的场景,以及需要在进程内方便地共享数据进行协同工作的情况,但在计算密集型任务上要考虑 GIL 的影响,对于多核性能提升有限。
  • 进程:更适合计算密集型任务,利用多核 CPU 并行计算来提高性能,同时对于一些需要高稳定性、容错性(进程间相对独立)的场景也比较适用,不过要注意处理好进程间通信的问题。
  • 作者:41楼的长颈鹿

    物联沃分享整理
    物联沃-IOTWORD物联网 » Python异步编程(协程)、线程、进程简介

    发表回复