Python Coroutine 池化实现

池化介绍

在当今计算机科学和软件工程的领域中,池化技术如线程池、连接池和对象池等已经成为优化资源利用率和提高软件性能的重要工具。然而,在 Python 的协程领域,我们却很少见到类似于 ThreadPoolExecutor 的 CoroutinePoolExecutor。为什么会这样呢?

首先,Python Coroutine 的特性使得池化技术在协程中的应用相对较少。与像 Golang 这样支持有栈协程的语言不同,Python Coroutine 是无栈的,无法跨核执行,从而限制了协程池发挥多核优势的可能性。

其次,Python Coroutine 的轻量级和快速创建销毁的特性,使得频繁创建和销毁协程并不会带来显著的性能损耗。这也解释了为什么 Python 官方一直没有引入 CoroutinePoolExecutor。

然而,作为开发者,我们仍然可以在特定场景下考虑协程的池化。虽然 Python Coroutine 轻量,但在一些需要大量协程协同工作的应用中,池化技术能够提供更方便、统一的调度子协程的方式。尤其是在涉及到异步操作的同时需要控制并发数量时,协程池的优势就显而易见了。

关于 Python 官方是否会在未来引入类似于 TaskGroup 的 CoroutinePoolExecutor,这或许是一个悬而未决的问题。考虑到 Python 在异步编程方面的快速发展,我们不能排除未来可能性的存在。或许有一天,我们会看到 TaskGroup 引入一个 max_workers 的形参,以更好地支持对协程池的需求。

在实际开发中,我们也可以尝试编写自己的 CoroutinePoolExecutor,以满足特定业务场景的需求。通过合理的设计架构和对数据流的全局考虑,我们可以最大程度地发挥协程池的优势,提高系统的性能和响应速度。

在接下来的文章中,我们将探讨如何设计和实现一个简单的 CoroutinePoolExecutor,以及在实际项目中的应用场景。通过深入理解协程池的工作原理,我们或许能更好地利用这一技术,使我们的异步应用更为高效。

如何开始编写

如何开始编写 CoroutinePoolExecutor,首先我们要明确出其适用范畴、考虑到使用方式和其潜在的风险点:

  • 它并不适用于 Mult Thread + Mult Event Loop 的场景,因此它并非线程安全的。
  • 应当保持和 ThreadPoolExecutor 相同的调用方式。
  • 不同于 Mult Thread 中子线程不依赖于主线程的运行,而在 Mult Coroutine 中子协程必须依赖于主协程,因此主协程在子协程没有全部运行完毕之前不能直接 done 掉。这也解释了为什么 TaskGroup 官方实现中没有提供类似于 shutdown 之类的方法,而是只提供上下文管理的运行方式。

有了上述 3 点的考量,我们决定将 ThreadPoolExecutor 平替成 CoroutinePoolExecutor。这样的好处在于,作为学习者一方面可以了解 ThreadPoolExecutor 的内部实现机制,另一方面站在巨人肩膀上的编程借鉴往往会事半功倍,对于自我的提升也是较为明显的。

在考虑这些因素的同时,我们将继续深入研究协程池的设计和实现。通过对适用范围和使用方式的明确,我们能更好地把握 CoroutinePoolExecutor 的潜在优势,为异步应用的性能提升做出更有针对性的贡献。

具体代码实现

在这里我先贴出完整的代码实现,其中着重点已经用注释标明。

以下是 CoroutinePoolExecutor 的代码实现:

import os
import asyncio
import weakref
import logging
import itertools async def _worker(executor_reference: "CoroutinePoolExecutor", work_queue: asyncio.Queue):
try:
while True:
work_item = await work_queue.get() if work_item is not None:
await work_item.run()
del work_item executor = executor_reference()
if executor is not None:
# Notify available coroutines
executor._idle_semaphore.release()
del executor
continue # Notifies the next coroutine task that it is time to exit
await work_queue.put(None)
break except Exception as exc:
logging.critical('Exception in worker', exc_info=True) class _WorkItem:
def __init__(self, future, coro):
self.future = future
self.coro = coro async def run(self):
try:
result = await self.coro
except Exception as exc:
self.future.set_exception(exc)
else:
self.future.set_result(result) class CoroutinePoolExecutor:
"""
Coroutine pool implemented based on ThreadPoolExecutor
Different from ThreadPoolExecutor, because the running of sub-coroutine depends on the main coroutine
So you must use the shutdown method to wait for all subtasks and wait for them to complete execution
""" # Used to assign unique thread names when coroutine_name_prefix is not supplied.
_counter = itertools.count().__next__ def __init__(self, max_workers, coroutine_name_prefix=""): if max_workers is None:
max_workers = min(32, (os.cpu_count() or 1) + 4)
if max_workers <= 0:
raise ValueError("max_workers must be greater than 0") self._max_workers = max_workers
self._work_queue = asyncio.Queue()
self._idle_semaphore = asyncio.Semaphore(0)
self._coroutines = set()
self._shutdown = False
self._shutdown_lock = asyncio.Lock()
self._coroutine_name_prefix = (coroutine_name_prefix or (
f"{__class__.__name__}-{self._counter()}"
)) async def submit(self, coro):
async with self._shutdown_lock:
# When the executor is closed, new coroutine tasks should be rejected, otherwise it will cause the problem that the newly added tasks cannot be executed.
# This is because after shutdown, all sub-coroutines will end their work
# one after another. Even if there are new coroutine tasks, they will not
# be reactivated.
if self._shutdown:
raise RuntimeError('cannot schedule new coroutine task after shutdown') f = asyncio.Future()
w = _WorkItem(
f,
coro
)
await self._work_queue.put(w)
await self._adjust_coroutine_count()
return f async def _adjust_coroutine_count(self): try:
# 2 functions:
# - When there is an idle coroutine and the semaphore is not 0, there is no need to create a new sub-coroutine.
# - Prevent exceptions from modifying self._coroutines members when the for loop self._coroutines and await task in shutdown are modified
# Since the Semaphore provided by asyncio does not have a timeout
# parameter, you can choose to use it with wait_for.
if await asyncio.wait_for(
self._idle_semaphore.acquire(),
0
):
return
except TimeoutError:
pass num_coroutines = len(self._coroutines)
if num_coroutines < self._max_workers:
coroutine_name = f"{self._coroutine_name_prefix or self}_{num_coroutines}"
t = asyncio.create_task(
coro=_worker(
weakref.ref(self),
self._work_queue
),
name=coroutine_name
) self._coroutines.add(t) async def shutdown(self, wait=True, *, cancel_futures=False):
async with self._shutdown_lock:
self._shutdown = True if cancel_futures:
while True:
try:
work_item = self._work_queue.get_nowait()
except asyncio.QueueEmpty:
break
if work_item is not None:
work_item.future.cancel() # None is an exit signal, given by the shutdown method, when the shutdown method is called
# will notify the sub-coroutine to stop working and exit the loop
await self._work_queue.put(None) if wait:
for t in self._coroutines:
await t async def __aenter__(self):
return self async def __aexit__(self, exc_type, exc_val, exc_tb):
await self.shutdown(wait=True)
return False

以下是 CoroutinePoolExecutor 的使用方式:

import asyncio

from coroutinepoolexecutor import CoroutinePoolExecutor

async def task(i):
await asyncio.sleep(1)
print(f"task-{i}") async def main():
async with CoroutinePoolExecutor(2) as executor:
for i in range(10):
await executor.submit(task(i)) if __name__ == "__main__":
asyncio.run(main())

我们知道,在线程池中,工作线程一旦创建会不断的领取新的任务并执行,除开 shutdown() 调用,否则对于静态的线程池来讲工作线程不会自己结束。

在上述协程池代码实现中,CoroutinePoolExecutor 类包含了主要的对外调用功能的接口、内部提供了存储 task 的 Queue、工作协程自动生成 name 的计数器、保障协程的信号量锁等等。

而 _worker 函数是工作协程的运行函数,其会在工作协程启动后,不断的从 CoroutinePoolExecutor 的 Queue 中得到 _WorkItem 并由 _WorkItem 具体执行 coro task。

剩下的 _WorkItem 是一个 future 对象与 coro task 的封装器,其功能是解耦 future 对象和 coro task、并在 coro task 运行时和运行后设置 future 的结果。

对于异步循环的思考

在此 CoroutinePoolExecutor 实现后,我其实又有了一个新的思考。Python 的 EventLoop 相较于 Node.js 的 EventLoop 来说其实更加的底层,它有感的暴露了出来。

具体体现在当 Python Event Loop 启动后,如果 main coroutine 停止运行,那么所有的 subtask coroutine 也会停止运行,尤其是对于一些需要清理资源的操作、如 aiohttp 的 close session、CoroutinePoolExecutor 的 shutdown 等都会在某些情况显得无措,说的更具体点就是不知道在什么时候调用。

对于这些问题,我们可以继承 BaseEventLoop 自己手动对 EventLoop 的功能进行扩展,如在事件循环关闭之前添加 hook function,甚至可以限制整个 EventLoop 的 max_workers 或者做成动态的可调节 coroutine 数量的 EventLoop 都行。

无论如何,只要心里有想法,就可以去将它实现 .. 学习本身就是一个不断挑战的过程。

Python Coroutine 池化实现的更多相关文章

  1. 字符串池化 python

    前言 在 Python 中经常通过内存池化技术来提高其性能,那么问题来了,在什么情况下会池化呢? 让我们通过几个例子进行一下理解一下. 预备知识 在查看例子之前,首先要提 python 中的一个函数 ...

  2. 【python实现卷积神经网络】池化层实现

    代码来源:https://github.com/eriklindernoren/ML-From-Scratch 卷积神经网络中卷积层Conv2D(带stride.padding)的具体实现:https ...

  3. 《转》python线程池

    线程池的概念是什么? 在IBM文档库中这样的一段描写:“在面向对象编程中,创建和销毁对象是很费时间的,因为创建一个对象要获取内存资源或者其它更多资源.在Java中更是 如此,虚拟机将试图跟踪每一个对象 ...

  4. 空间金字塔池化(Spatial Pyramid Pooling, SPP)原理和代码实现(Pytorch)

    想直接看公式的可跳至第三节 3.公式修正 一.为什么需要SPP 首先需要知道为什么会需要SPP. 我们都知道卷积神经网络(CNN)由卷积层和全连接层组成,其中卷积层对于输入数据的大小并没有要求,唯一对 ...

  5. tensorflow 1.0 学习:池化层(pooling)和全连接层(dense)

    池化层定义在 tensorflow/python/layers/pooling.py. 有最大值池化和均值池化. 1.tf.layers.max_pooling2d max_pooling2d( in ...

  6. TensorFlow池化层-函数

    池化层的作用如下-引用<TensorFlow实践>: 池化层的作用是减少过拟合,并通过减小输入的尺寸来提高性能.他们可以用来对输入进行降采样,但会为后续层保留重要的信息.只使用tf.nn. ...

  7. tensorflow的卷积和池化层(二):记实践之cifar10

    在tensorflow中的卷积和池化层(一)和各种卷积类型Convolution这两篇博客中,主要讲解了卷积神经网络的核心层,同时也结合当下流行的Caffe和tf框架做了介绍,本篇博客将接着tenso ...

  8. tensorflow中的卷积和池化层(一)

    在官方tutorial的帮助下,我们已经使用了最简单的CNN用于Mnist的问题,而其实在这个过程中,主要的问题在于如何设置CNN网络,这和Caffe等框架的原理是一样的,但是tf的设置似乎更加简洁. ...

  9. 1.字符串池化(intern)机制及拓展学习

    1.字符串intern机制 用了这么久的python,时刻和字符串打交道,直到遇到下面的情况: a = "hello" b = "hello" print(a ...

  10. 基于深度学习和迁移学习的识花实践——利用 VGG16 的深度网络结构中的五轮卷积网络层和池化层,对每张图片得到一个 4096 维的特征向量,然后我们直接用这个特征向量替代原来的图片,再加若干层全连接的神经网络,对花朵数据集进行训练(属于模型迁移)

    基于深度学习和迁移学习的识花实践(转)   深度学习是人工智能领域近年来最火热的话题之一,但是对于个人来说,以往想要玩转深度学习除了要具备高超的编程技巧,还需要有海量的数据和强劲的硬件.不过 Tens ...

随机推荐

  1. PS组件相关BAPI

    一.新增组件BAPI "-----------------------------------------@斌将军-------------------------------------- ...

  2. Deltix Round, Spring 2021 (open for everyone, rated, Div. 1 + Div. 2) (ABCE补题记录)

    补题链接:Here 1523A. Game of Life 生命游戏定义 本题中改编为一维坐标上的生命游戏 即使 \(m(m\in[1,1e9])\) 的范围很大,但每次进化不会超过 \(n\) 次, ...

  3. 四、swift大对象--静态态大对象

    系列导航 一.swift对象存储环境搭建 二.swift添加存储策略 三.swift大对象--动态大对象 四.swift大对象--静态态大对象 五.java操作swift对象存储(官网样例) 六.ja ...

  4. [Violation] Added non-passive event listener to a scroll-blocking 'mousewheel' event.

    在基于 Element-ui 写项目的时候,Chrome 提醒: [Violation] Added non-passive event listener to a scroll-blocking ' ...

  5. 接口自动化测试复习巩固第二天,管理员后端验证和接口抓包+requests实现

    接口自动化测试第二天,需要用到的第三方库有os,openpyxl,json,pytest,requests 首选我们今天的目标是写出一个测试登录用例的脚本,这里我用的是分层设计,整个框架暂时被分为工具 ...

  6. Linux系列之文件和目录权限

    前言 我们知道,root用户基本上可以在系统中做任何事.其他用户有更多的限制,并且通常被收集到组中.你把有类似需求的用户放入一个被授予相关权限的组,每个成员都继承组的权限. 让我们看一下: 查看权限( ...

  7. 为什么 sort() 中的 return a-b 可以决定升序

    arr.sort( function(a,b){ return a-b; } ) 千万不要理解成 a 减 b 其实它代表的是26个字母中的 a 和 b b 比 a 大,所以 a - b 就是升序,写成 ...

  8. [转帖]Linux下非oracle用户如何使用系统认证方式登陆数据

    https://www.cnblogs.com/kerrycode/p/17772866.html Linux系统中,DBA一般使用oracle用户登陆/访问Linux操作系统,然后使用sqlplus ...

  9. [转帖]prometheus和node_exporter中的磁盘监控

    https://www.ipcpu.com/2021/04/prometheus-node_exporter/ prometheus和node_exporter中的磁盘监控.md 对于磁盘问题,我们主 ...

  10. [转帖]062、监控指标之PD

    PD相关 Grafana 监控 PD -> PD Dashboard 是否存在异常状态的TiKV Grafana 监控 PD -> Region health 大表清理后,出现了大量的空r ...