代码环境:python3.6

到目前为止,我们介绍的多任务编程方式就包括了:


上一篇介绍的协程是个好东西,理论上所有多线程处理的 IO 密集型应用,都可以换成协程来处理,并且协程在数据使用上更安全、资源占用更小,所以协程完全可以替代多线程吗?

很可惜不是的。最直接的一个问题是,到目前 2020 年为止,依然不是所有的 python 库都提供了异步方法。所以,在实际工作中,这几种方式都是根据实际情况搭配着使用的:

  • CPU 密集型应用,使用多进程;
  • IO 密集型应用,优先考虑协程;涉及到还没提供异步方法的库,则用多线程。

从 python3.2 版本开始,我们可以使用concurrent.futures模块进行多任务编程。这个模块是对我们前面使用的多进程、多线程以及协程相关的常用模块进一步抽象得到的,目的是让我们更方便使用 python 处理并发,这也是现代 python 多任务的常用写法。

实际工作中,对处理多任务简单的做法是:

  1. 启动程序后,分别创建一个进程池、线程池和EventLoop
  2. EventLoop负责调度一切协程(协程内避免阻塞);
  3. 遇到阻塞的调用时,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 切换开销,需要根据实际情况进行控制。


oldk
3 声望2 粉丝