代码环境:python3.6
到目前为止,我们介绍的多任务编程方式就包括了:
上一篇介绍的协程是个好东西,理论上所有多线程处理的 IO 密集型应用,都可以换成协程来处理,并且协程在数据使用上更安全、资源占用更小,所以协程完全可以替代多线程吗?
很可惜不是的。最直接的一个问题是,到目前 2020 年为止,依然不是所有的 python 库都提供了异步方法。所以,在实际工作中,这几种方式都是根据实际情况搭配着使用的:
- CPU 密集型应用,使用多进程;
- IO 密集型应用,优先考虑协程;涉及到还没提供异步方法的库,则用多线程。
从 python3.2 版本开始,我们可以使用concurrent.futures
模块进行多任务编程。这个模块是对我们前面使用的多进程、多线程以及协程相关的常用模块进一步抽象得到的,目的是让我们更方便使用 python 处理并发,这也是现代 python 多任务的常用写法。
实际工作中,对处理多任务简单的做法是:
- 启动程序后,分别创建一个进程池、线程池和
EventLoop
; -
EventLoop
负责调度一切协程(协程内避免阻塞); - 遇到阻塞的调用时,IO 密集型的扔进线程池,CPU 密集型的扔进进程池。
这样代码逻辑简单,还能尽可能的利用机器性能。 一个简单的完整示例:
from multiprocessing import Manager, Queue
from concurrent.futures import ProcessPoolExecutor, ThreadPoolExecutor
from asyncio import get_event_loop, sleep as asleep, gather
from time import sleep, time
from functools import wraps
N_PROCESS = 4
N_THREADS = 10
thread_executor = ThreadPoolExecutor(max_workers=N_THREADS)
process_executor = ProcessPoolExecutor(max_workers=N_PROCESS)
aioloop = get_event_loop()
def timer(func):
@wraps(func)
def wrapper(*args, **kw):
print(f"{func.__name__} running...")
start_at = time()
try:
return func(*args, **kw)
finally:
print(f"{func.__name__} end, cost {time() - start_at:.2f}s")
return wrapper
def async_timer(func):
@wraps(func)
async def wrapper(*args, **kw):
print(f"{func.__name__} running...")
start_at = time()
try:
return await func(*args, **kw)
finally:
print(f"{func.__name__} end, cost {time() - start_at:.2f}s")
return wrapper
@timer
def io_blocking_task():
"""I/O 型阻塞调用"""
sleep(1)
@timer
def cpu_blocking_task():
"""CPU 型阻塞调用"""
for _ in range(1 << 26):
pass
@async_timer
async def coroutine_task():
"""异步协程调用"""
await asleep(1)
@async_timer
async def coroutine_error():
"""会抛出异常的协程调用"""
raise AttributeError("This is an Exception")
@async_timer
async def coroutine_main():
"""一般我们会写一个coroutine的main函数,专门负责管理协程"""
r = await gather(
coroutine_task(),
coroutine_error(),
aioloop.run_in_executor(thread_executor, io_blocking_task),
aioloop.run_in_executor(process_executor, cpu_blocking_task),
return_exceptions=True,
)
print(f"coroutine_main got {r}")
@timer
def main():
aioloop.run_until_complete(coroutine_main())
if __name__ == "__main__":
main()
在进行多任务编程时,我们要注意系统资源的控制,一般来说主要关注 缓冲区 和 并发量。
缓冲区一般用Queue
的大小来控制,并发量则是同时执行的进程数或线程数。过高的缓冲区会提高内存消耗量,过高的并发量则会增加 CPU 切换开销,需要根据实际情况进行控制。