跳到主要内容 Python 并发编程:多线程、多进程与协程详解 | 极客日志
Python
Python 并发编程:多线程、多进程与协程详解 Python 并发编程涉及多线程、多进程及异步协程三种主要方式。CPU 密集型任务适合多进程以利用多核并行,I/O 密集型任务适合多线程或协程。CPython 解释器存在全局解释器锁(GIL),限制多线程在 CPU 计算上的并行能力。通过 multiprocessing 模块可实现多进程,threading 模块配合 Lock 类可解决线程安全问题。进程池和线程池能优化资源管理。asyncio 库提供基于事件循环的异步编程模型,适用于高并发 I/O 场景。开发者需根据任务特性选择合适的并发策略并进行性能调优。
www 发布于 2025/2/7 更新于 2026/4/19 1 浏览在 Python 中,并发编程的实现有多种方式,包括多线程、多进程和异步编程。每一种方式都有其使用的场景和特点。那么如何去选择多线程、多进程和多协程呢?要知道如何选择的话就要了解一下什么是 CPU 密集型计算、什么是 I/O 密集型计算;多线程、多进程和多协程又有什么样的区别。
CPU 密集型和 I/O 密集型 CPU 密集型(CPU-bound) :CPU 密集型任务指的是任务的主要负载在 CPU 上,而不是在输入/输出操作上。这种类型的任务通常涉及大量的计算、循环和逻辑操作,而且不需要频繁地进行文件读写、网络通信等操作。例如:压缩解压缩、加密解密、正则表达式搜索。
I/O 密集型(I/O bound) :IO 密集型任务指的是任务的主要负载在输入/输出操作上,而不是在 CPU 计算上。这种类型的任务涉及到频繁的文件读写、网络通信、数据库操作等 IO 操作,而在执行这些操作时,CPU 的利用率相对较低。例如:文件处理程序,网络爬虫程序,读写数据库程序。
多线程、多进程和协程对比 一个进程中可以启动 N 个线程,一个线程中可以启动 N 个协程;多进程、多线程、多协程三种技术中只有多进程能够同时利用多核 CPU 并行计算。
多线程、多进程和多协程是处理并发任务的常见方式,在不同的场景下具有各自的优势和适用性。下面是它们的对比:
多线程 :
优点:线程轻量,创建和销毁开销较小;可以在一个进程中实现并发执行,共享进程的内存空间,可直接访问共享数据;适合 I/O 密集型任务。
缺点:线程间共享内存可能引发竞态条件和死锁等并发问题;全局解释器锁(GIL)限制了 CPython 中多线程的并行性能。
适用场景:I/O 密集型任务,如网络通信、文件读写等。
多进程 :
优点:各自独立的内存空间,通过进程间通信(IPC)以及操作系统调度,可以实现真正的并行执行;可以利用多核 CPU 资源;适合 CPU 密集型任务。
缺点:创建和销毁进程开销较大;进程间通信的代价较高。
适用场景:CPU 密集型任务,如图像处理、数据分析等。
多协程 :
优点:协程是一种轻量级的线程,可以在同一个线程内实现并行执行;不需要线程切换的开销;适合 I/O 密集型任务;适用于构建高效的异步 IO 应用程序。
缺点:协程需要显式地在代码中进行调度和切换,需要避免长时间阻塞的操作。
适用场景:I/O 密集型任务,如网络爬虫、Web 服务器、消息队列等。
总结来说,多线程适合处理 I/O 密集型任务,多进程适合处理 CPU 密集型任务,而多协程适用于构建高效的异步 IO 应用程序。在选择适合的并发处理方式时,需要考虑任务的特性、执行效率、资源占用等因素,并进行性能测试和调优。
Python 全局解释性锁 (GIL 锁) GIL(Global Interpreter Lock),全局解释器锁,是 CPython 解释器中的一个机制。GIL 的作用 是确保在同一时刻只有一个线程执行 Python 字节码。这意味着在 CPython 中,即使使用多个线程,也不能真正实现多线程的并行执行。
GIL 的存在 是为了保护 CPython 解释器内部的数据结构不被并发访问 而导致的竞态条件 。竞态条件 是指多个线程在没有适当同步的情况下对共享数据进行读写操作,可能导致数据的不一致性和错误的结果。
因为有 GIL 的限制,多线程在 CPU 密集型任务中不能充分利用多核 CPU 资源,只有在 I/O 密集型任务中才能获得一定的性能提升。原因是,当线程阻塞在 IO 操作上时,GIL 会释放,允许其他线程执行。但在实际情况中,如果任务主要涉及 CPU 计算,那么 GIL 会成为性能瓶颈,因为同一时刻只能有一个线程在执行。
为了充分利用多核 CPU,可以使用多进程来代替多线程,每个进程有自己独立的解释器和 GIL。因此,多进程可以实现真正的并行执行,适合 CPU 密集型任务。
需要注意的是,GIL 只存在于 CPython 解释器 中。其他实现,如 Jython(Java 平台)、IronPython(.NET 平台)或 PyPy,没有 GIL 的限制,并可以实现真正的多线程并行执行。此外,GIL 只会影响到 Python 的解释器层级,并不影响通过 C、C++ 等语言编写的扩展模块的多线程执行。
多进程实现 在 Python 中,可以使用多进程来实现并发编程。Python 提供了 multiprocessing 模块,方便地创建和管理进程。
import multiprocessing
def worker ():
print ("Worker process" )
process = multiprocessing.Process(target=worker)
process.start()
import multiprocessing
def worker (pipe ):
msg = pipe.recv()
print ("Worker process received:" , msg)
parent_conn, child_conn = multiprocessing.Pipe()
process = multiprocessing.Process(target=worker, args=(child_conn,))
process.start()
parent_conn.send("Hello from parent process" )
process.join()
import multiprocessing
def worker (x ):
return x * x
pool = multiprocessing.Pool()
result = pool.apply_async(worker, (10 ,))
print (result.get())
pool.close()
pool.join()
需要注意的是,在多进程编程中,每个进程都有独立的内存空间 ,因此进程间的数据无法直接共享 。如果需要在进程间共享数据,可以使用 Pipe、Queue、Manager 等机制来实现进程间通信。
此外,由于 Python 中的全局解释锁(GIL)限制,多进程适用于 CPU 密集型任务,而对于 I/O 密集型任务,通常使用多线程或异步编程效果更好。
多线程实现 在 Python 中,多线程是通过 threading 模块实现的。这个模块提供了创建、管理线程以及线程间通信的功能。
以下是在 Python 中使用多线程编程的一般步骤:
my_thread = threading.Thread(target=my_function)
这里通过 threading.Thread 类创建了一个线程对象 my_thread,并将要执行的函数 my_function 作为目标函数,也可以传递参数给目标函数。
调用线程对象的 start 方法来启动线程,线程将开始执行 my_function 函数中的任务逻辑。
join 方法用于阻塞主线程,等待子线程执行完毕。这样可以保证在主线程退出之前,子线程已经完成。
线程安全 :多个线程同时访问共享数据可能导致数据竞争和不一致性。可以使用锁、条件变量等机制来保证线程安全。
全局解释器锁(GIL) :在 CPython 解释器中,全局解释器锁限制了同一时刻只能有一个线程执行 Python 字节码。这意味着 Python 的多线程在 CPU 密集型任务中效果不佳,适合 I/O 密集型任务。
线程间通信 :多个线程之间可能需要进行数据交换和协调工作。可以使用队列或者共享变量等方式进行线程间通信。
死锁 :当多个线程都在等待某个资源,而不释放自己的资源时,则可能发生死锁。需要小心设计和调试,避免死锁问题的发生。
异常处理 :在线程中的异常默认会被忽略,无法通过 try-except 语句捕获。可以通过 threading.excepthook 设置全局的线程异常处理函数来捕获线程中的异常。
总的来说,Python 的多线程编程相对容易上手,但需要注意线程安全、GIL 和线程间通信等问题。在实际应用中,需要根据任务类型和性能要求来选择合适的多线程或多进程编程方式。
使用 Lock 类处理解决线程安全问题 存在两种方式对发生线程安全的代码进行加锁:① try-finally 模式;② with 模式。
import threading
lock = threading.Lock()
lock.acquire()
try :
...
finally :
lock.release()
import threading
lock = threading.Lock()
with lock:
...
接下来通过一个取钱的例子来进行学习 (以下案例为虚构案例,并非实际情况)。
老王和妻子共同使用同一张银行卡并绑定了电子账户,卡上原本的余额为 1000 元,在老王消费 800 元的同时妻子也正消费 800,此时两人同时提交了支付请求,但是还没有来到扣费请求,那么也就意味着此时余额减少,此时二者的请求在判断的时候都是满足余额大于消费金额的,所以就可能造成 最终的余额进行了两次 800 的减少,则最终导致余额成了 -600,发生了不可预料的后果。
import threading
import time
class Account :
def __init__ (self, balance ):
self .balance = balance
def draw (account, amount ):
if account.balance >= amount:
time.sleep(0.1 )
print (threading.current_thread().name + "取钱成功" )
account.balance -= amount
print (threading.current_thread().name + " 余额:" , account.balance)
else :
print (threading.current_thread().name, "取钱失败,余额不足" )
if __name__ == '__main__' :
account = Account(1000 )
ta = threading.Thread(name="a" , target=draw, args=(account, 800 ))
tb = threading.Thread(name="b" , target=draw, args=(account, 800 ))
ta.start()
tb.start()
那如果同一张卡两人同时消费的时候我们加上一个在同一时刻只有一人能够进行余额减少的逻辑,也就是说二者虽然同时提交了消费请求但是二者的扣费逻辑肯定是有一个先后扣费的顺序的,当老王的请求先到达扣费逻辑时,老王妻子的扣费请求便必须等老王的扣费请求结束之后才能够实现。其实这个逻辑就是在代码中进行加锁实现的。
import threading
import time
lock = threading.Lock()
class Account :
def __init__ (self, balance ):
self .balance = balance
def draw (account, amount ):
with lock:
if account.balance >= amount:
time.sleep(0.1 )
print (threading.current_thread().name + "取钱成功" )
account.balance -= amount
print (threading.current_thread().name + " 余额:" , account.balance)
else :
print (threading.current_thread().name, "取钱失败,余额不足" )
if __name__ == '__main__' :
account = Account(1000 )
ta = threading.Thread(name="a" , target=draw, args=(account, 800 ))
tb = threading.Thread(name="b" , target=draw, args=(account, 800 ))
ta.start()
tb.start()
进程池 进程池是一种用于管理和调度进程的技术。它通过预先创建一组可重用的进程,以便在需要时分配任务给这些进程来执行。这种方式可以减少进程创建和销毁的开销,并提高任务处理的效率。
Python 中的 multiprocessing 模块提供了进程池的实现。使用进程池可以实现以下功能:
from multiprocessing import Pool
pool = Pool(processes=4 )
在这个示例中,使用 Pool 类创建了一个包含 4 个进程的进程池。
result = pool.apply_async(func, args)
使用 apply_async 方法可以将任务提交给进程池,并返回一个表示任务执行结果的对象。
使用 get 方法可以获取任务的结果。如果任务还没有完成,主进程将在此处阻塞,直到任务完成并返回结果。
使用 close 方法关闭进程池后,将不会再接受新的任务。然后使用 join 方法等待所有任务完成。
通过使用进程池,可以简化并发任务的管理和调度,提高程序的执行效率。进程池在处理大量任务时特别有用,因为可以利用多个进程并行处理任务,从而加快任务的处理速度。但是,进程池也有一些限制,比如需要更多的系统资源,比如内存,而且进程间的通信相对复杂。因此,在选择使用进程池时,应该根据实际需求权衡利弊。
线程池 线程池是一种用于管理和复用线程的技术。它通过预先创建一组可复用的线程,以便在需要时分配任务给这些线程来执行。这种方式可以避免线程创建和销毁的开销,并提高任务处理的效率。
Python 中的 concurrent.futures 模块提供了线程池的实现。使用线程池可以实现以下功能:
from concurrent.futures import ThreadPoolExecutor
pool = ThreadPoolExecutor(max_workers=4 )
在这个示例中,使用 ThreadPoolExecutor 类创建了一个包含 4 个线程的线程池。
result = pool.submit(func, *args, **kwargs)
使用 submit 方法可以将任务提交给线程池,并返回一个表示任务执行结果的 Future 对象。
使用 result 方法可以获取任务的结果。如果任务还没有完成,主线程将在此处阻塞,直到任务完成并返回结果。
使用 shutdown 方法关闭线程池后,将不会再接受新的任务。线程池会等待所有已提交的任务完成,然后终止所有线程。
通过使用线程池,可以简化并发任务的管理和调度,提高程序的执行效率。线程池适用于 I/O 密集型任务,如网络请求、文件读写等操作,可以充分利用多个线程并发执行任务,而不会受到全局解释器锁(GIL)的限制。但是,线程池也有一些限制,比如需要更多的系统资源,如内存,而且线程间的通信相对复杂。因此,在选择使用线程池时,应该根据实际需求权衡利弊。
异步 I/O asyncio(异步 IO)是 Python 中用于编写异步程序的标准库。它提供了一种基于协程(coroutine)的异步编程模型,可以处理大量并发任务,实现高效的并发和并行操作。
协程(Coroutines) :asyncio 使用协程来编写异步任务。协程是一种轻量级的非抢占式并发模型,它通过使用关键字 async 来定义异步函数,使用 await 来挂起协程并等待结果。
import asyncio
async def hello ():
print ("Hello" )
await asyncio.sleep(1 )
print ("World" )
asyncio.run(hello())
在上述示例中,hello 函数是一个异步函数,使用 async 关键字进行定义。await asyncio.sleep(1) 可以挂起协程并等待 1 秒钟,然后再继续执行。
事件循环(Event Loop) :asyncio 使用事件循环来调度和管理并发任务。事件循环是一个无限循环,不断地从任务队列中获取待执行的任务,调度协程的执行。
import asyncio
async def hello ():
print ("Hello" )
await asyncio.sleep(1 )
print ("World" )
loop = asyncio.get_event_loop()
loop.run_until_complete(hello())
在上述示例中,通过 asyncio.get_event_loop() 获取一个事件循环对象,然后使用 run_until_complete 方法来运行协程。
异步 IO 操作 :asyncio 提供了一系列的异步 IO 操作函数,如文件读写、网络请求等。这些函数都是协程函数,可以通过 await 来等待异步操作完成。
import asyncio
async def download_data (url ):
async with aiohttp.ClientSession() as session:
async with session.get(url) as response:
data = await response.read()
return data
asyncio.run(download_data("http://example.com" ))
在上述示例中,download_data 函数使用 aiohttp 库来进行异步网络请求。通过 await 来等待响应的数据,并最终返回结果。
并发和并行 :asyncio 可以处理大量并发任务,并实现高效的并发和并行操作。将多个协程封装到 asyncio.gather 函数中,可以同时运行多个协程。
import asyncio
async def task1 ():
pass
async def task2 ():
pass
loop = asyncio.get_event_loop()
tasks = [task1(), task2()]
loop.run_until_complete(asyncio.gather(*tasks))
在上述示例中,通过 asyncio.gather 函数同时运行多个协程,*tasks 将任务列表作为参数传递给 gather 函数。
asyncio 的优势在于它可以让程序员使用简单的语法来实现高效和可维护的异步代码。它提供了全面的异步 IO 支持,可以处理各种异步操作。然而,使用 asyncio 编写代码需要理解协程和事件循环的概念,以及异步编程带来的一些复杂性。同时,要注意避免阻塞事件循环,以充分发挥 asyncio 的优势。
微信扫一扫,关注极客日志 微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
相关免费在线工具 curl 转代码 解析常见 curl 参数并生成 fetch、axios、PHP curl 或 Python requests 示例代码。 在线工具,curl 转代码在线工具,online
Base64 字符串编码/解码 将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
Base64 文件转换器 将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
Markdown转HTML 将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online
HTML转Markdown 将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online
JSON 压缩 通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online