Python 并发编程指南:协程 vs 多线程及其他模型比较
Python 并发编程指南:协程 vs 多线程及其他模型比较
并发编程是指在单个程序中同时处理多个任务的能力,这些任务可以交替进行(同一时刻并不一定真的同时运行),而并行则强调在同一时刻真正同时运行多个任务(通常需要多个CPU核) (GlobalInterpreterLock - Python Wiki)。Python提供了多种并发模型,包括多线程、多进程和协程等,它们各有不同的运行机制和适用场景。在CPython解释器中,由于**全局解释器锁(GIL)**的存在,同一时刻只有一个线程能执行Python字节码 (threading — Thread-based parallelism — Python 3.13.2 documentation)。因此,我们需要根据任务类型(I/O密集型还是CPU密集型)选择合适的并发策略,从而提高程序性能和响应速度。
下面将详细比较Python中的协程(基于asyncio
)与多线程(threading
)的使用方法、运行机制、示例代码和应用场景,并介绍其他相关的并发概念,如多进程、GIL、线程池/进程池等。
Python 多线程(Threading)基础
多线程是指在单一进程内创建多个线程,让它们看似同时执行。每个线程由操作系统调度,线程之间共享进程的内存空间,因此可以方便地共享数据。但是,在CPython实现中,全局解释器锁限制了多线程的并行执行:一次只能有一个线程执行Python代码 (threading — Thread-based parallelism — Python 3.13.2 documentation)。这意味着对于CPU密集型的纯Python任务,多线程并不能利用多核优势,线程反而会串行执行。但对于I/O密集型任务(如文件读写、网络请求),线程在等待I/O时会释放GIL,使得其他线程可以运行,从而实现并发效率 (GlobalInterpreterLock - Python Wiki)。正如官方文档所建议,如果想充分利用多核CPU,应考虑使用多进程或进程池,而多线程更适合同时运行多个I/O绑定的任务 (threading — Thread-based parallelism — Python 3.13.2 documentation)。
**使用方法:**Python提供了threading
标准库来管理线程。可以通过创建threading.Thread
实例并指定目标函数来启动新线程。每个线程可以执行相同或不同的任务,主线程可以使用join()
等待所有子线程完成。下面是一个简单示例,演示启动多个线程并发执行:
import threading
import timedef worker(name):print(f"线程 {name} 开始")time.sleep(2) # 模拟I/O操作print(f"线程 {name} 结束")# 创建并启动3个线程
threads = []
for i in range(3):t = threading.Thread(target=worker, args=(i+1,))threads.append(t)t.start()# 等待所有线程完成
for t in threads:t.join()
print("所有线程任务已完成")
在以上代码中,我们创建了3个线程来执行worker
函数。每个线程启动后都会打印开始信息,等待2秒(模拟某种I/O等待),然后打印结束信息。由于使用了多线程,这三个任务会并发执行——总的运行时间约为2秒多一点,而不是顺序执行所需的6秒。这表明多线程能够有效地重叠等待时间,从而提高I/O密集任务的吞吐量。
运行机制:多线程由操作系统内核调度,利用时间片轮转等策略实现任务切换。在线程切换时,CPU寄存器、调用栈等上下文需要保存和恢复,这就带来了上下文切换开销。相比进程,线程的上下文切换开销较小,因为线程共享进程的大部分资源。然而,Python的GIL会使线程调度变为串行化(对于CPU计算),即使底层操作系统可能将线程分配到不同CPU核上,GIL也会确保同一时刻只有一个线程在执行Python字节码 (threading — Thread-based parallelism — Python 3.13.2 documentation)。值得注意的是,一些性能导向的扩展库(如NumPy的底层C代码)可以释放GIL,从而在多线程情况下实现真正的并行计算 (threading — Thread-based parallelism — Python 3.13.2 documentation)。总体来说,多线程非常适合I/O密集型任务,例如等待网络响应或文件读写,因为当一个线程阻塞等待I/O时,其他线程仍可继续运行。对于这些场景,多线程可以显著提升程序的响应能力和吞吐量 (GlobalInterpreterLock - Python Wiki)。
适用场景:由于GIL的限制,多线程并不适用于加速纯Python的CPU密集运算(例如大量数学计算、图像处理等),此时开启多个线程反而可能增加开销并不能缩短执行时间。然而,在需要同时处理许多I/O操作时,多线程是简便且有效的选择。例如,一个爬虫程序可以开启多个线程抓取网页;GUI程序可以使用后台线程执行耗时任务以避免阻塞主线程的界面;在网络服务器中,可以采用线程来处理不同客户端的请求(典型的线程池模型)。需要注意的是,多线程编程中涉及线程安全问题:多个线程访问共享数据时需要使用锁(Lock)、信号量等同步原语来防止竞态条件和数据不一致。这增加了编程复杂度,也可能因不当使用锁导致死锁等问题。开发者在使用多线程时需要仔细设计线程间的通信与同步机制。
Python 协程(Asyncio)基础
协程是一种基于程序级别的轻量级“线程”,与操作系统线程不同,协程由程序本身调度。在Python中,协程通常通过asyncio
库并结合async/await
语法来实现。协程的运行基于事件循环:事件循环在单一线程中反复调度各个协程任务,只有在协程明确挂起(await
某个异步操作)时才切换到其他任务。由于切换由程序控制且无需操作系统内核干预,协程切换的开销远小于线程的上下文切换开销 (Python中的多线程与协程的比较与应用场景-阿里云开发者社区)。协程通过这种合作式调度实现并发——任务在需要等待时主动让出控制权,从而在单线程内实现高效的并发执行 (Python 多线程、多进程与协程的对比与应用-CSDN博客)。Python的asyncio
正是提供了这种事件驱动的协程并发模型,它无需创建真正的OS线程即可并发执行大量任务 (threading — Thread-based parallelism — Python 3.13.2 documentation)。
**使用方法:**要定义一个协程函数,使用async def
关键字;在协程内部可以使用await
调用其他协程或等待异步I/O操作。为了运行协程,需要一个事件循环,例如使用asyncio.run()
来执行最高层的协程函数,或者在事件循环中创建任务。下面是一个简单的协程示例,演示如何使用asyncio
并发执行两个异步任务:
import asyncioasync def say_after(delay, what):await asyncio.sleep(delay)print(what)async def main():# 并发启动两个协程任务task1 = asyncio.create_task(say_after(1, "hello"))task2 = asyncio.create_task(say_after(2, "world"))print("任务开始")# 等待两个任务完成await task1await task2print("任务结束")asyncio.run(main())
在这个示例中,say_after
是一个协程,它等待给定的秒数后打印一句话。main
函数中,我们通过asyncio.create_task
并发调度了两个协程任务:一个等待1秒后打印“hello”,另一个等待2秒后打印“world”。由于它们是在同一事件循环中并发执行的,整个程序约在2秒后就完成了所有任务(比顺序等待3秒快) (Coroutines and Tasks — Python 3.13.2 documentation) (Coroutines and Tasks — Python 3.13.2 documentation)。运行结果中可以看到“hello”大约在启动1秒后打印,“world”在2秒后打印,最后打印“任务结束”。这表明协程成功实现了并发:在等待I/O(这里通过asyncio.sleep
模拟)时,事件循环切换到了其他任务执行。
运行机制:asyncio
通过单线程的事件循环来调度协程任务。协程本质上是一种用户级线程,只有在执行await
时才会主动让出控制权。因此,协程的并发是非抢占式的:一个协程运行期间除非遇到await
,否则不会被自动中断。这意味着编写协程代码时需要确保适当使用await
使任务及时让出控制,以防某个协程长时间占用线程而阻塞了整个事件循环。由于所有协程都在一个线程中执行,协程之间无需像线程那样加锁防止数据竞争——在事件循环的调度下,同一时刻只有一个协程在运行,避免了多线程锁竞争和上下文切换开销 (Python中的多线程与协程的比较与应用场景-阿里云开发者社区)。协程非常适合处理大量I/O密集型任务:例如高并发的网络请求、socket通信、文件读写等场景,在这些场景下协程相比多线程可以显著减少由于线程切换带来的开销 (Python 多线程、多进程与协程的对比与应用-CSDN博客)。需要注意,如果协程中执行了大量纯计算且没有适当的await
让出控制,整个事件循环都会被阻塞(因为仍然只有一个底层线程)。因此对于CPU密集型任务,协程并不能利用多核并行,往往需要搭配多进程或将计算密集部分放到线程池/进程池中执行。
适用场景:协程的优势在于处理高并发的I/O场景。典型例子包括:基于asyncio
的网络服务器或客户端(同时处理成百上千个连接)、网络爬虫(并发抓取大量网页)、聊天服务器、即时通讯应用等。当有大量任务主要在等待I/O时,协程能够以单线程下的极小开销调度上万级别的并发任务。在这些情况下,协程往往比创建等量的线程更加高效和节省资源 (Python中的多线程与协程的比较与应用场景-阿里云开发者社区)。另外,协程采用声明式的async/await
语法,使异步流程看起来像同步代码,避免了传统回调函数方式的回调地狱,代码可读性更好。不过,协程也有其局限:对于需要利用多核进行并行计算的工作,它无法像多进程那样真正并行;同时对初学者来说,引入async/await
会增加一些学习成本,一些现有的阻塞库也需要异步替代方案才能在协程中使用。总的来说,如果任务涉及大量并发I/O且每个任务自身的CPU开销不大,协程是非常理想的选择。
协程 vs 多线程:差异对比
协程和多线程都是实现并发的手段,但它们在实现机制、性能特点和使用方式上有显著差异:
- 底层实现:多线程依赖操作系统内核调度,使用原生线程并发执行;协程则完全在用户空间由事件循环调度,在单线程内实现并发 (threading — Thread-based parallelism — Python 3.13.2 documentation)。这意味着协程不会真正创建新的OS线程,它通过程序逻辑在需要时切换任务。
- 调度方式:多线程是抢占式并发——操作系统可以在任意时刻中断和切换线程;协程是合作式并发——只有当协程代码主动
await
时才发生切换。协程避免了线程抢占造成的同步问题,但也要求开发者合理插入await
以防止“独占”事件循环。 - 上下文切换开销:线程切换由OS完成,需要保存/恢复寄存器和栈等,开销相对较高;协程切换只是函数栈的切换,由于运行在同一线程,开销极小,非常轻量 (Python中的多线程与协程的比较与应用场景-阿里云开发者社区)。因此在大量并发任务情况下(成百上千任务),协程的资源占用和调度开销往往比等量线程要低得多 (Python中的多线程与协程的比较与应用场景-阿里云开发者社区)。
- 并行能力(CPU利用率):由于GIL存在,CPython中多个线程无法同时在多个核上执行Python字节码 (threading — Thread-based parallelism — Python 3.13.2 documentation)——多线程并不能提升CPU密集型任务的吞吐(除非调用释放GIL的计算例程)。协程本身也是在单线程运行,同样无法利用多核并行。所以无论线程还是协程,对于CPU密集型任务,必须借助多进程才能真正并行利用多核。如果是I/O密集型任务,多线程和协程都能够并发执行并提高总体吞吐,但协程可以用更少的开销处理更多的并发连接数 (Python中的多线程与协程的比较与应用场景-阿里云开发者社区)。
- 编程难度:多线程需要考虑线程间共享数据的同步,涉及锁、信号量、线程安全队列等,容易出现死锁、竞态等问题,调试复杂 (Python中的多线程与协程的比较与应用场景-阿里云开发者社区)。协程因为单线程执行,消除了大部分此类问题,逻辑上更接近顺序执行,使用
async/await
编写异步代码相对直观。不过,引入协程需要学习新的编程范式,调试协程调度问题也有一定难度(如意外阻塞事件循环的问题)。 - 典型应用场景:多线程常用于I/O操作适中且需要简化并发编程的场景,例如同时下载多个文件、并行执行多个独立任务等。协程则擅长极高并发I/O的场景,例如高并发网络服务器、爬虫等,需要同时处理大量socket连接。需要强调的是,如果任务涉及大量计算(CPU瓶颈),无论使用线程还是协程,都应考虑拆分任务到多进程或原生扩展,以绕过GIL限制 (Python中的多线程与协程的比较与应用场景-阿里云开发者社区)。
总之,协程和多线程各有千秋:协程胜在极高并发下的轻量级调度和避免锁开销,而多线程则在处理少量并发且需要直接利用现有同步机制或与阻塞IO库交互时比较方便。选择哪种模型应根据任务性质权衡:I/O密集且并发数量特别大倾向于协程,I/O密集但并发量不高或需要与现有非异步代码集成时可以使用多线程。而在CPU密集型任务上,这两者的差异反而次要,因为都无法利用多核,需要引入其他并发手段(如多进程)。
Python 多进程(Multiprocessing)
多进程是另一种并发模型,它通过在操作系统层面创建多个进程来实现真正的并行执行。Python提供了multiprocessing
模块,使我们能够方便地启动子进程并在其中运行任务。每个子进程有独立的Python解释器和全局解释器锁,因此多个进程可以在不同CPU核上同时执行Python代码,实现真正的并行计算 (Python 多线程、多进程与协程的对比与应用-CSDN博客)。这使得多进程非常适合CPU密集型的任务,因为它能够绕过GIL限制,充分利用多核硬件资源 (Python 多线程、多进程与协程的对比与应用-CSDN博客)。例如,在进行复杂的计算(矩阵运算、数据分析)或图像处理时,使用多进程可以显著缩短运行时间。
使用方法: 可以直接使用multiprocessing.Process
来创建进程,类似于threading的接口,也可以使用更高级的multiprocessing.Pool
或后面介绍的concurrent.futures.ProcessPoolExecutor
创建进程池。下面示例展示如何启动多个进程来并行执行任务:
from multiprocessing import Process
import timedef worker(name):print(f"进程 {name} 开始 (PID={os.getpid()})")time.sleep(2) # 模拟耗时操作print(f"进程 {name} 结束 (PID={os.getpid()})")processes = []
for i in range(3):p = Process(target=worker, args=(i+1,))processes.append(p)p.start()for p in processes:p.join()
print("所有进程任务已完成")
在这个例子中,我们创建了3个子进程分别执行worker
函数。每个子进程都会输出自身的开始和结束信息(包含进程ID)。由于是不同的进程,这些任务可真正同时运行在多个CPU上,因此总运行时间约为2秒左右,而非顺序执行需要的6秒。子进程运行完后,我们使用join()
等待它们结束。可以看到,多进程用法和多线程十分类似,但底层实现差别很大:每个进程有独立的内存空间和解释器状态,变量不能直接在不同进程间共享(需要通过队列、管道或共享内存等机制进行进程间通信IPC)。
运行机制:操作系统通过进程调度器分配CPU时间给不同进程。进程间相互独立,一个进程的崩溃不会直接影响另一个。这种隔离带来了更高的健壮性和安全性,但是也导致更高的资源开销:创建进程比创建线程开销大,需要分配新的内存空间,操作系统需要维护额外的PCB(进程控制块)等信息。另外,进程间数据交换必须通过IPC,通信成本高于线程共享内存的方式 (Python 多线程、多进程与协程的对比与应用-CSDN博客)。因此,虽然多进程能够利用多核并行执行,但在任务粒度很小、需要频繁共享数据的场景下,多进程的效率可能不如多线程或协程。
适用场景:多进程最适合CPU密集型的工作,例如大规模的计算任务、数据处理、机器学习模型训练等。这些场景下,每个任务需要大量CPU时间,而且各任务之间交互不频繁、数据共享较少。通过多进程可以让每个CPU核承担一部分工作,从而线性加速总体计算。同时,由于进程间彼此独立,多进程也用于一些需要隔离执行的场景,比如将不同客户任务放在不同进程以防止崩溃互相影响,或者在服务器中使用多个进程(而非线程)来处理请求以利用多核并提高健壮性。一些Web服务器(如使用Gunicorn运行多进程的WSGI应用)就是这种模型。不过,对于I/O密集型任务,多进程并不一定优于多线程或协程——因为I/O操作本身不会占满CPU,多进程的并行能力无用武之地,反而增加了进程切换和IPC的开销。因此,选择多进程需要权衡:仅当需要绕过GIL执行CPU繁重任务或要求更高隔离时才使用,其他情况下可能有更轻量的并发方案。
全局解释器锁(GIL)的影响
**全局解释器锁(Global Interpreter Lock, GIL)**是CPython解释器中的一个机制,它确保同一时刻只有一个线程执行Python字节码。GIL实际上是一个互斥锁,保护着Python的内部对象状态,防止多线程同时访问修改这些状态,从而保证了解释器执行的线程安全 (GlobalInterpreterLock - Python Wiki)。换句话说,在CPython中,无论有多少个线程,同时只能有一个线程持有GIL在运行Python代码,其它线程即使在不同CPU核上也处于等待状态。这对Python多线程并发的影响是深远的:
- 限制CPU并行:GIL使得Python的多线程无法利用多核CPU同时执行多个线程的字节码。当线程数增加时,CPU计算密集的程序并不会像预期那样加速,可能还因为线程调度开销略有下降甚至变慢。举个例子,如果一个任务纯粹计算密集(如计算大量素数),使用10个线程和1个线程的性能可能相差无几,因为10个线程实际上仍是在串行地争用同一个CPU核运行。正因为如此,Python官方文档明确指出:若想充分利用多核,应采用多进程或进程池而非多线程 (threading — Thread-based parallelism — Python 3.13.2 documentation)。
- I/O密集情况下的作用:在I/O操作中(如磁盘读写、网络收发),线程执行I/O操作时会释放GIL,使得其他线程可以获得GIL继续执行 (GlobalInterpreterLock - Python Wiki)。此外,许多执行耗时工作的C扩展库也会在执行期间释放GIL,例如进行矩阵运算的NumPy、图像处理的OpenCV等。这意味着对于I/O密集型任务,GIL对多线程并发性能的影响并不明显:一个线程等待I/O时另一个线程可以运行,多个线程各自等待不同的I/O操作能够实现真正的并发 (GlobalInterpreterLock - Python Wiki)。因此我们看到诸如基于多线程的Web爬虫、网络服务器在I/O密集场景下仍然能有效并发运行。
- 线程竞争与性能:即便在I/O场景,GIL也会带来一些额外开销。当有多个线程频繁地获取和释放GIL时,GIL本身的锁开销会导致性能下降。在极端情况下,GIL可能造成线程颠簸(thrashing):线程不断地切换但大部分时间都在竞争锁而非真正做有用工作。这种情况通常发生在混合了I/O和CPU操作的多线程程序中。不过,总的来说,如果多数时间线程都在等待I/O,GIL的开销是可以忽略的,而如果线程都在执行Python计算,则不如使用多进程来并行。
- 设计初衷:GIL最初是CPython为了简化内存管理和确保线程安全而引入的历史决策 (GlobalInterpreterLock - Python Wiki)。它极大地降低了Python实现的复杂度,使得在没有细粒度锁的情况下也能编写多线程代码(只要注意Python级别的同步即可)。但是它也成为了Python在多核并行计算方面的瓶颈。这些年来社区提出过多次移除GIL的方案,但由于GIL的存在与众多扩展模块的实现细节息息相关,彻底移除一直很困难 (GlobalInterpreterLock - Python Wiki)。
GIL的未来:值得一提的是,Python官方正在探索取消GIL的可能。在即将发布的Python 3.13中,引入了一种可选的“无GIL”解释器构建(称为“free threading”实验特性),允许在编译Python时禁用GIL (Python experimental support for free threading — Python 3.13.2 documentation)。在无GIL模式下,多线程可以真正并行执行,充分利用多核CPU (Python experimental support for free threading — Python 3.13.2 documentation)。不过,目前无GIL版本仍处于实验阶段,其单线程性能有所下降,兼容性也未完全成熟 (Python experimental support for free threading — Python 3.13.2 documentation)。PEP 703 提案详细描述了让GIL可选化的设想 (Python experimental support for free threading — Python 3.13.2 documentation)。如果这一特性在将来正式加入主线Python,多线程的并行能力将大大提升。然而在现阶段(Python 3.12及以前稳定版本),CPython的GIL仍然是客观存在的制约因素。因此,编写并发程序时需要充分考虑GIL的影响:对于CPU密集任务使用多进程或本地扩展,而对于I/O密集任务则可以放心地使用多线程或协程来实现并发。
线程池和进程池(concurrent.futures)
手动创建和管理线程或进程对于大量小任务来说可能比较繁琐,而且频繁创建销毁线程/进程也有一定开销。Python的concurrent.futures
模块提供了线程池和进程池接口,方便地批量执行并发任务。其核心是两个类:ThreadPoolExecutor
和ProcessPoolExecutor
,分别用于管理线程池和进程池。这两个类实现了相同的接口(都是Executor
的子类),因此使用方法也几乎相同 (concurrent.futures — Launching parallel tasks — Python 3.13.2 documentation)。
线程池(ThreadPoolExecutor):线程池内部维护着固定数量的工作线程,调用者可以将函数提交给线程池,池中的线程会自动获取任务执行。当有大量独立的小任务需要并发执行时,使用线程池可以避免为每个任务创建销毁线程的开销。线程池特别适用于I/O密集型任务的批量并发,比如同时下载几十个网页、并行处理多个文件读写等。在Python中,线程池依然受GIL限制,意味着如果任务函数主要执行Python计算,线程池中的任务实际上还是串行运行的。因此经验法则是:I/O 密集型任务用线程池,而CPU 密集型任务不要用线程池 (ThreadPoolExecutor vs ProcessPoolExecutor in Python - Super Fast Python)(除非每个任务内部调用了释放GIL的扩展函数)。下面是线程池的使用示例:
from concurrent.futures import ThreadPoolExecutor, as_completed
import time# 定义一个简单的任务函数
def task(n):time.sleep(1) # 模拟I/O等待return n * n# 使用线程池执行多个任务
with ThreadPoolExecutor(max_workers=3) as executor:futures = [executor.submit(task, i) for i in range(5)]for future in as_completed(futures):result = future.result()print(f"计算结果: {result}")
以上代码创建了一个最大工作线程数为3的线程池,并提交了5个任务(计算数字的平方,包含1秒的模拟等待)。线程池会调度最多3个任务并发运行,其它任务排队等待线程空闲。通过as_completed
我们可以按任务完成顺序获取结果并打印。运行时可以看到,尽管总共有5个任务,每个任务睡眠1秒,但因为有3个线程并发执行,总耗时比顺序执行明显减少。线程池的好处是我们无需手工启动和管理每个线程,ThreadPoolExecutor
会重用线程来执行多个任务,从而降低频繁创建线程的开销。
进程池(ProcessPoolExecutor):进程池和线程池的接口一致,只是内部使用子进程来执行任务。对于CPU密集型的并行计算,进程池可以充分利用多核优势,将任务分配给多个进程同时运行 (ThreadPoolExecutor vs ProcessPoolExecutor in Python - Super Fast Python)。使用进程池时,需要确保任务函数及其参数是可序列化(picklable)的,因为底层实现会通过序列化数据将任务发送给子进程执行。以下是将上述示例改为使用进程池的方式:
from concurrent.futures import ProcessPoolExecutorwith ProcessPoolExecutor(max_workers=3) as executor:futures = [executor.submit(task, i) for i in range(5)]for future in as_completed(futures):result = future.result()print(f"计算结果: {result}")
可以看到,除了将类名改为ProcessPoolExecutor
之外,代码与线程池版本几乎相同。事实上,这种统一的接口使得我们可以很方便地在线程池和进程池之间切换:如果后来发现任务是CPU密集型的,只需改用进程池即可。需要注意,进程池启动时会预先创建子进程(数量取决于max_workers
),这一步相对耗时且占用资源,因此对于非常短小的任务,进程池的收益可能被启动开销抵消。线程池则没有跨进程通信的开销,适合处理快速的I/O任务。总的来说,线程池适合I/O型并发批处理,进程池适合CPU型并行计算 (ThreadPoolExecutor vs ProcessPoolExecutor in Python - Super Fast Python)。
除了submit/as_completed
之外,Executor还提供了map
方法,可以更简洁地并行映射函数。例如:executor.map(task, range(5))
会并发地将task
应用到0-4五个参数上,并返回一个结果迭代器。无论线程池还是进程池,都大大简化了并发编程的工作,使我们更专注于要并发执行的任务本身。
不同并发模型的优缺点和典型应用
下面将各种并发模型的特点作一个总结,方便根据需求选择合适的方案:
-
多线程 (Threading):优点是在单进程内并发,线程间共享内存数据,切换开销小于进程。对于I/O密集型任务,多个线程可以在等待I/O时并发执行其他操作,提高效率 (GlobalInterpreterLock - Python Wiki)。线程编程相对直观,Python的线程库使用也较简单。缺点是CPython的GIL限制了其在CPU密集型任务上的并行能力 (threading — Thread-based parallelism — Python 3.13.2 documentation),无法利用多核提升速度。此外,多线程需要处理同步和锁机制,稍有不慎会出现竞态条件或死锁,调试难度大。在Web爬虫、文件读写并发、网络服务等主要受限于I/O的场景,多线程是常用且有效的方案。
-
协程 (Asyncio):优点是极高的并发能力和资源效率。在单线程里就能调度成千上万的协程任务,开销远小于成千上万的线程 (Python中的多线程与协程的比较与应用场景-阿里云开发者社区)。协程避免了线程同步问题,代码风格接近同步逻辑,维护性好。非常适合高并发I/O场景(例如高并发网络服务器、聊天服务器、爬虫等),能以很小的线程数处理海量并发连接。缺点是不能利用多核并行CPU计算,如果有耗时的计算任务会阻塞整个事件循环。此外,引入协程需要学习异步编程模型,调试思维与同步有所不同。一些阻塞库无法直接在协程中使用(需有对应的异步库或使用线程池封装)。典型应用如基于
aiohttp
的异步Web服务、异步爬虫等,它们通常面临大量并发I/O且每个请求处理耗时很短,这正是协程大显身手之处。 -
多进程 (Multiprocessing):优点是可以利用多核CPU实现真正的并行,加速CPU重负荷任务 (Python 多线程、多进程与协程的对比与应用-CSDN博客)。各进程独立运行,彼此隔离,提高健壮性,某个子进程崩溃不影响主进程。适合CPU密集型的计算,如科学计算、图像处理、大数据分析等,通过划分任务到多个进程可近似线性地缩短执行时间。缺点是进程创建和上下文切换开销大于线程,进程间共享数据需要序列化传输,编程模型相对复杂。过多的进程也可能因为争夺硬件资源而降低整体性能。典型应用场景包括使用
multiprocessing
并发计算、Web服务器采用多进程模式处理请求(如Gunicorn启动多个Worker进程)、任务执行器将独立任务分配给多个进程并行完成等。当需要最大化CPU利用率且任务之间相对独立时,多进程是有效的选择。例如,在进行矩阵运算或机器学习训练时,可用多进程将不同数据块分配到多个核并行处理;又比如批量处理图像时,每个进程处理部分图像,实现总体加速 (Python 多线程、多进程与协程的对比与应用-CSDN博客)。 -
线程池:线程池本质上是对多线程的封装管理。优点是简化了使用多个线程的流程,自动控制线程的数量和生命周期,适合一次性并发很多短小任务的情况。通过重用线程资源,降低了频繁创建销毁线程的成本。当有海量小型I/O任务时(例如并发调用很多外部API),线程池可以高效地调度执行。其限制仍然是GIL,因此对于CPU密集任务,多线程池不会比单线程快。一般来说,线程池用于I/O密集的批处理任务,让我们不用关心线程同步和调度,只关注任务本身。缺点方面,线程池的任务函数如果处理不好仍可能出现死锁等问题(例如在线程池任务中再次发起阻塞调用)。另外,需要注意不要将耗时巨大的计算放入线程池,否则线程长期占用CPU会降低并发效果。
-
进程池:进程池是对多进程的封装管理。优点是同样简化了使用多进程的流程,屏蔽了IPC细节,自动维护一定数量的子进程空闲等待任务,避免频繁创建进程的开销。适用于CPU密集型的并行任务批处理,例如对一组数据分别做复杂计算,将这些计算分发到进程池的不同进程并行完成。由于使用多进程,它可以绕过GIL限制,在多核上实现加速。进程池的缺点包括:启动过程较慢,占用更多内存,任务函数和数据需可序列化,过于频繁的小任务用进程池不划算等。总体而言,在需要并行加速计算且任务之间相对独立时,进程池提供了比手动管理进程更方便的接口。
典型应用选择示例:如果我们在实现一个高并发的聊天服务器,连接数成千上万且主要开销在网络I/O,那么使用协程(asyncio)无疑是最佳选择,可以在单线程中处理大量并发连接 (Python 多线程、多进程与协程的对比与应用-CSDN博客)。如果我们在进行图像滤镜处理批量照片,每张处理都需要大量CPU计算,则宜采用多进程或进程池将不同照片的处理分布到多个进程上并行进行 (Python 多线程、多进程与协程的对比与应用-CSDN博客)。对于同时下载100个文件的任务,线程池或协程都可以胜任:如果使用现成阻塞的HTTP库,线程池更直接;如果使用异步HTTP库,那么协程可取得更高效率。总之,应根据任务的性质(I/O还是CPU密集)、并发量以及代码维护成本来选择模型。很多情况下还可以混合使用:例如主程序使用asyncio
协程处理网络I/O,但在需要做CPU密集型计算时,使用loop.run_in_executor
将任务提交到进程池执行,从而兼顾了高并发和CPU并行。理解并善用这些并发工具,有助于我们写出高性能且高扩展性的Python应用。
参考文献:
- Python官方文档 - Threading 模块说明 (threading — Thread-based parallelism — Python 3.13.2 documentation) (threading — Thread-based parallelism — Python 3.13.2 documentation)
- Real Python: What Is the Python Global Interpreter Lock (GIL)? (GlobalInterpreterLock - Python Wiki) (GlobalInterpreterLock - Python Wiki)
- CSDN博客:《Python 多线程、多进程与协程的对比与应用》 (Python 多线程、多进程与协程的对比与应用-CSDN博客) (Python 多线程、多进程与协程的对比与应用-CSDN博客) (Python 多线程、多进程与协程的对比与应用-CSDN博客)
- 阿里云开发者社区:《Python中的多线程与协程:比较与应用场景》 (Python中的多线程与协程的比较与应用场景-阿里云开发者社区) (Python中的多线程与协程的比较与应用场景-阿里云开发者社区) (Python中的多线程与协程的比较与应用场景-阿里云开发者社区)
- SuperFastPython: ThreadPoolExecutor vs ProcessPoolExecutor 使用建议
相关文章:
Python 并发编程指南:协程 vs 多线程及其他模型比较
Python 并发编程指南:协程 vs 多线程及其他模型比较 并发编程是指在单个程序中同时处理多个任务的能力,这些任务可以交替进行(同一时刻并不一定真的同时运行),而并行则强调在同一时刻真正同时运行多个任务(…...
WPS JS宏编程教程(从基础到进阶)-- 第五部分:JS数组与WPS结合应用
目录 摘要第5章 JS数组与WPS结合应用5-1 JS数组的核心特性核心特性解析5-2 数组的两种创建方式(字面量与扩展操作符)1. 字面量创建2. 扩展操作符创建5-3 数组创建应用:提取字符串中的数字需求说明代码实现5-4 用函数创建数组(new Array、Array.of、Array.from)1. new Arra…...
STM32定时器完全指南:从基础原理到高级应用 | 零基础入门STM32第九十六步
主题内容教学目的/扩展视频TIM定时器重点课程定时器,捕获器,比较器,PWM,单脉冲。高级TIM。定时器中断。了解TIM使用 师从洋桃电子,杜洋老师 📑文章目录 一、定时器核心原理1.1 硬件架构解析1.2 核心参数公式…...
Kafka分区机制详解:原理、策略与应用
#作者:张桐瑞 文章目录 一、分区的作用二、分区策略(一)轮询策略(二)随机策略(三)按消息键保序策略 三、实际案例:消息顺序问题的解决四、其他分区策略:基于地理位置的分…...
最小K个数
文章目录 题意思路代码 题意 题目链接 思路 代码 class Solution { public:vector<int> smallestK(vector<int>& arr, int k) {priority_queue<int> Q;for (auto &index:arr){Q.push(index);if (Q.size() > k)Q.pop();}vector<int> ans…...
【STL】list介绍(附与vector的比较)
文章目录 1.关于list2.使用2.1 list的构造2.2 list 迭代器的使用2.3 list 容量操作2.3.1 size()2.3.2 empty()2.3.3 resize() 2.4 list 元素访问2.4.1 front()2.4.2 back() 2.5 list 修改操作2.5.1 push_front()2.5.2 pop_front()2.5.3 push_back()2.5.4 pop_back()2.5.5 inser…...
音视频生命探测仪,救援现场的“视听先锋”|鼎跃安全
地震等自然灾害的突发性和破坏性对人类生命构成严重威胁。据统计,地震后的“黄金72小时”内,被困者的存活率随时间的推移急剧下降,因此快速、精准的搜救技术至关重要。 传统搜救手段依赖人耳识别呼救声或手动挖掘,效率低且易造成二…...
Arch视频播放CPU占用高
Arch Linux配置视频硬件加速 - DDoSolitary’s Blog 开源神器:加速你的视频体验 —— libvdpau-va-gl-CSDN博客 VDPAU(Video Decode and Presentation API for Unix) VA-API(Video Acceleration API) OpenGL 我的电…...
Python技巧:二维列表 和 二维矩阵 的区别
np.vstack 是 NumPy 中的一个函数,用于将多个数组沿垂直方向(行方向)堆叠。它可以处理 二维列表 和 二维矩阵,但它们之间有一些关键区别。以下是详细说明: 1. 二维列表 定义: 二维列表是 Python 原生的数据结构&#x…...
Linux 命令清单(Linux Command List)
测试人员必备的 Linux 命令清单文件管理 ls —— 显示目录内容。 ls -l 使用 -l 选项查看详细信息。 cd —— 改变当前工作目录。 cd /path/to/directory mkdir —— 创建新目录。 mkdir new_directory rm —— 删除文件或目录。 rm filename rm -r directory 使用 …...
Wallaby‘s: Nightmare (v1.0.2)靶场渗透
Wallabys: Nightmare (v1.0.2) 来自 <Wallabys: Nightmare (v1.0.2) ~ VulnHub> 1,将两台虚拟机网络连接都改为NAT模式 2,攻击机上做namp局域网扫描发现靶机 nmap -sn 192.168.23.0/24 那么攻击机IP为192.168.23.182,靶场IP192.168.23…...
java基础 可拆分迭代器 Spliterator<T>
Spliterator Spliterator介绍核心方法tryAdvanceforEachRemainingtrySplitestimateSizetrySplit 结合并行流(Parallel Stream)关键注意事项总结 Spliterator介绍 Spliterator(Splittable Iterator)是 Java 8 引入的接口ÿ…...
【AI提示词】决策专家
提示说明 决策专家可以帮助你进行科学决策,尽可能避免错误,提升决策成功的概率。 提示词 # Role : 决策专家决策,是面对不容易判断优劣的几个选项,做出正确的选择。说白了,决策就是拿个主意。决策专家是基于科学决策…...
VectorBT量化入门系列:第二章 VectorBT核心功能与数据处理
VectorBT量化入门系列:第二章 VectorBT核心功能与数据处理 本教程专为中高级开发者设计,系统讲解VectorBT技术在量化交易中的应用。通过结合Tushare数据源和TA-Lib技术指标,深度探索策略开发、回测优化与风险评估的核心方法。从数据获取到策略…...
Spring Boot 配置文件加载优先级全解析
精心整理了最新的面试资料和简历模板,有需要的可以自行获取 点击前往百度网盘获取 点击前往夸克网盘获取 Spring Boot 配置文件加载优先级全解析 Spring Boot 的配置文件加载机制是开发者管理不同环境配置的核心功能之一。其通过外部化配置(Externaliz…...
System V 信号量:控制进程间共享资源的访问
System V 信号量:控制进程间共享资源的访问 在多进程操作系统中,当多个进程需要共享资源时,必须确保对资源的访问是有序的,以避免竞争条件(Race Condition)和数据不一致性问题。System V 信号量࿰…...
海运货代系统哪家好?能解决了哪些常见管理难题?
随着跨境电商的迅速发展,货代行业在全球供应链中扮演着越来越重要的角色。随着市场需求的多样化和国际运输环境的复杂化,货代企业面临的挑战也愈发复杂。为了应对这些挑战,数字化管理工具成为货代行业不可或缺的一部分。如今先进的海运货代系…...
预测性维护+智能优化:RK3568的储能双保险
在碳中和目标推动下,储能行业正经历前所未有的发展机遇。作为储能系统的核心组件,储能柜的智能化水平直接影响着整个系统的效率和安全性。RK3568智慧边缘控制器凭借其强大的计算能力、丰富的接口和高效的能源管理特性,正在成为工商储能柜的&q…...
蓝桥20257-元宵分配
#include <iostream> #include <bits/stdc.h> using namespace std; const int N1e910; typedef long long LL; int main() {// 请在此输入您的代码//将强其中的一碗全部倒进另一个中,将所有汤圆排序,最后选择前(N/2)…...
How to connect a mobile phone to your computer?
How to connect a mobile phone to your computer? 1. Background /ˈbkɡraʊnd/2. How to connect a mobile phone to your computer?References 1. Background /ˈbkɡraʊnd/ Let me introduce the background first. Today we will talk about this topic: How to conn…...
【力扣刷题实战】全排列II
大家好,我是小卡皮巴拉 文章目录 目录 力扣题目:全排列II 题目描述 解题思路 问题理解 算法选择 具体思路 解题要点 完整代码(C) 兄弟们共勉 !!! 每篇前言 博客主页:小卡…...
题目练习之map的奇妙使用
♥♥♥~~~~~~欢迎光临知星小度博客空间~~~~~~♥♥♥ ♥♥♥零星地变得优秀~也能拼凑出星河~♥♥♥ ♥♥♥我们一起努力成为更好的自己~♥♥♥ ♥♥♥如果这一篇博客对你有帮助~别忘了点赞分享哦~♥♥♥ ♥♥♥如果有什么问题可以评论区留言或者私信我哦~♥♥♥ ✨✨✨✨✨✨ 个…...
Excel 日期值转换问题解析
目录 问题原因 解决方案 方法1:使用 DateTime.FromOADate 转换 方法2:处理可能为字符串的情况 方法3:使用 ExcelDataReader 时的处理 额外提示 当你在 Excel 单元格中看到 2024/12/1,但 C# 读取到 45627 时,这是…...
Linux--文件系统
ok,上次我们提到了硬件和inode,这次我们继续学习文件系统 ext2文件系统 所有的准备⼯作都已经做完,是时候认识下文件系统了。我们想要在硬盘上存储文件,必须先把硬盘格式化为某种格式的文件系统,才能存储文件。文件系…...
2025 年福建交安安全员考试:结合本省交通特点备考
福建地处东南沿海,交通建设具有独特特点,这对交安安全员考试备考意义重大。在桥梁建设方面,由于面临复杂的海洋环境,桥梁的防腐、防台风等安全措施成为重点。考生在学习桥梁施工安全知识时,要特别关注福建本地跨海大桥…...
【项目管理】第6章 信息管理概论 --知识点整理
项目管理 相关文档,希望互相学习,共同进步 风123456789~-CSDN博客 (一)知识总览 项目管理知识域 知识点: (项目管理概论、立项管理、十大知识域、配置与变更管理、绩效域) 对应&…...
python-leetcode 66.寻找旋转排序数组中的最小值
题目: 已知一个长度为n的数组,预先按照升序排列,经由1到n次旋转后,得到输入数组,例如,原数组 nums [0,1,2,4,5,6,7] 在变化后可能得到: 若旋转 4 次,则可以得到 [4,5,6,7,0,1,2]若…...
WinMerge下载及使用教程(附安装包)
文章目录 一、WinMerge安装步骤1.WinMerge下载:2.解压:3.启动: 二、WinMerge使用步骤1.添加文件或文件夹2.查看差异3.格式选择 WinMerge v2.16.36 是一款免费开源的文件与文件夹比较、合并工具,能帮您快速找出差异,提高…...
Codeforces Round 1011 (Div. 2)
Dashboard - Codeforces Round 1011 (Div. 2) - Codeforces Problem - B - Codeforces 题目大意: 给你一个数组,你可以用一段子序列中没有出现的最小非负整数,替换数组中的组序列,经过若干操作,让数组变为长度为1,值…...
深度学习实战105-利用LSTM+Attention模型做生产车间中的铝合金生产时的合格率的预测应用
大家好,我是微学AI,今天给大家介绍一下深度学习实战105-利用LSTM+Attention模型做生产车间中的铝合金生产时的合格率的预测应用。 本项目利用LSTM+Attention模型对铝合金生产合格率进行预测,不仅在理论上具有创新性和可行性,而且在实际应用中也具有重要的价值和广阔的应用前…...
苹果内购支付 Java 接口
支付流程,APP支付成功后 前端调用后端接口,后端接口将前端支付成功后拿到的凭据传给苹果服务器检查,如果接口返回成功了,就视为支付。 代码,productId就是苹果开发者后台提前设置好的 产品id public CommonResult<S…...
Scrapy 是什么?Python 强大的爬虫框架详解
1. Scrapy 简介 Scrapy 是一个用 Python 编写的开源 网络爬虫框架,用于高效地从网站提取结构化数据。它提供了完整的爬虫开发工具,包括请求管理、数据解析、存储和异常处理等功能,适用于数据挖掘、监测和自动化测试等场景。 Scrapy 的核心特…...
一种用于基于扩散磁共振成像(MRI)的微观结构估计的外梯度与噪声调谐自适应迭代网络|文献速递-深度学习医疗AI最新文献
Title 题目 An extragradient and noise-tuning adaptive iterative network for diffusionMRI-based microstructural estimation 一种用于基于扩散磁共振成像(MRI)的微观结构估计的外梯度与噪声调谐自适应迭代网络 Background 背景 2.1. Advanced…...
需求的图形化分析-状态转换图
实时系统和过程控制应用程序可以在任何给定的时间内以有限的状态存在。当满足所定义的标准时,状态就会发生改变,例如在特定条件下,接收到一个特定的输入激励。这样的系统是有限状态机的例子。此外,许多业务对象(如销售…...
3月AI论文精选十篇
1. Feature-Level Insights into Artificial Text Detection with Sparse Autoencoders[1] 核心贡献:通过稀疏自编码器揭示AI生成文本的检测特征,提出基于特征分布的鉴别方法。研究发现,AI文本在稀疏编码空间中呈现独特的"高频低幅"…...
【android bluetooth 框架分析 01】【关键线程 2】【bt_stack_manager_thread线程介绍】
1. bt_stack_manager_thread bt_stack_manager_thread 是蓝牙协议栈中的核心调度线程,负责串行化处理协议栈的生命周期事件,包括初始化、启动、关闭与清理操作。它确保这些状态切换在同一线程中按顺序执行,避免竞态和资源冲突。作为蓝牙栈的…...
GEO, TCGA 等将被禁用?!这40个公开数据库可能要小心使用了
GEO, TCGA 等将被禁用?!这40个公开数据库可能要小心使用了 最近NIH公共数据库开始对中国禁用的消息闹得风风火火: 你认为研究者上传到 GEO 数据库上的数据会被禁用吗? 单选 会,毕竟占用存储资源 不会,不…...
matlab安装python API 出现Invalid version: ‘R2022a‘,
打开 setup.py 文件,找到设置版本号的部分 将 versionR2022a 修改为符合 Python 版本号规范的格式,例如 version2022.1 保存 setup.py 文件...
【ROS 通信】Services 服务通信
【ROS】Service 服务通信 前言前置操作创建一个 tutorial 功能包定义服务接口修改 CMakeLists.txt 文件修改 find_package修改 add_service_files修改 generate_messages修改 catkin_packagefind_package 和 catkin_package 修改 package.xml 文件构建 服务通信的 Python 实现服…...
25.4.8学习总结
javaFX实现倒计时 核心概念 Timeline: Timeline 是JavaFX动画API的核心类,用于创建动画。它可以按照指定的时间间隔(Duration)触发事件(KeyFrame)。 可以将其视为一个定时器,每隔一段时间执行一些操作。 …...
Android audio(6)-audiopolicyservice介绍
AudioPolicyService 是策略的制定者,比如某种 Stream 类型不同设备的音量(index/DB)是多少、某种 Stream 类型的音频数据流对应什么设备等等。而 AudioFlinger 则是策略的执行者,例如具体如何与音频设备通信,维护现有系…...
【区块链安全 | 第三十八篇】合约审计之获取私有数据(二)
文章目录 前言漏洞代码代码审计攻击步骤修复/开发建议审计思路 前言 在【区块链安全 | 第三十七篇】合约审计之获取私有数据(一)中,介绍了私有数据、访问私有数据实例、Solidity 中的数据存储方式等知识,本文通过分析具体合约代码…...
muduo:运行起来
Muduo 概述 Muduo 是一个用 C 编写的高性能网络库,由陈硕开发,主要用于开发 Linux 环境下的高性能网络应用程序。以下从几个方面对其进行详细介绍: 特点 事件驱动与非阻塞 I/O:Muduo 基于 Reactor 模式实现,使用了 …...
算法篇(八)【递归】
一、了解递归 1. 什么是递归? 递归就是自己调用自己 递归的概念解释起来就短短的几句话,但是写起来总是无从下手 ,但是首先要相信,在学过了数据结构 -- 树 之后 , 其实就已经具备了一定的递归思想,接下来的…...
Linux 学习笔记(4):cd 与 pwd 命令的深度解析与实战应用(期末、期中复习必备)
前言 一、cd 命令:切换工作目录的利器 1.命令来源与基本语法 2.命令使用示例 3.相对路径与绝对路径的使用 二、pwd 命令:清晰定位当前工作目录 1.命令来源与基本语法 2.命令使用示例 三、结语 前言 在 Linux 系统的操作中,对工作目录的…...
眨眼睛查看密码工具类
“眨眼睛查看密码”工具类实现思路: 一、核心功能 实现点击眼睛图标切换密码明文/星号显示,提升表单输入体验。包含以下关键功能: • 初始状态:密码框显示为星号,闭眼图标可见。 • 点击闭眼图标:切换为明…...
【嵌入式系统设计师】知识点:第9章 嵌入式系统安全性基础知识
提示:“软考通关秘籍” 专栏围绕软考展开,全面涵盖了如嵌入式系统设计师、数据库系统工程师、信息系统管理工程师等多个软考方向的知识点。从计算机体系结构、存储系统等基础知识,到程序语言概述、算法、数据库技术(包括关系数据库、非关系型数据库、SQL 语言、数据仓库等)…...
find指令中使用正则表达式
linux查找命令能结合正则表达式吗 find命令要使用正则表达式需要结合-regex参数 另,-type参数可以指定查找类型(f为文件,d为文件夹) rootlocalhost:~/regular_expression# ls -alh 总计 8.0K drwxr-xr-x. 5 root root 66 4月 8日 16:26 . dr-xr-…...
【RH124】第六章 管理本地用户和组
系列文章目录 第一章 红帽企业Linux入门 第二章 访问命令行 第三章 从命令行管理文件 第五章 创建、查看文本文件 第六章 管理本地用户和组 文章目录 系列文章目录前言一、用户和组1、用户2、组 二、获取超级用户访问权限1、root用户2、切换用户账户3、sudo配置 三、管理本地用…...
Linux学习笔记——中断
中断 硬中断和软中断的定义与区别硬中断(Hardware Interrupt)软中断(Software Interrupt) 硬中断与软中断的区别总结上半部和下半部机制详解为什么要分为上半部和下半部?上半部下半部 下半部的三种实现机制Linux中断响…...