Python 如何执行“两个”;“聚合”;函数(如sum)并发运行,从同一迭代器提供它们?
假设我们有一个迭代器,比如说Python 如何执行“两个”;“聚合”;函数(如sum)并发运行,从同一迭代器提供它们?,python,iterator,async-await,coroutine,Python,Iterator,Async Await,Coroutine,假设我们有一个迭代器,比如说iter(范围(1000))。我们有两个函数,每个函数都接受一个迭代器作为唯一的参数,比如sum()和max()。在SQL世界中,我们称它们为聚合函数 是否有任何方法可以在不缓冲迭代器输出的情况下获得这两者的结果 为此,我们需要暂停并恢复聚合函数的执行,以便在不存储它们的情况下为它们提供相同的值。也许有一种方法可以用不睡觉的异步事物来表达它吗?让我们考虑如何将两个聚合函数应用到同一个迭代器中,我们只能一次使用它。最初的尝试(为了简洁起见,硬编码sum和max,但可以简
iter(范围(1000))
。我们有两个函数,每个函数都接受一个迭代器作为唯一的参数,比如sum()
和max()
。在SQL世界中,我们称它们为聚合函数
是否有任何方法可以在不缓冲迭代器输出的情况下获得这两者的结果
为此,我们需要暂停并恢复聚合函数的执行,以便在不存储它们的情况下为它们提供相同的值。也许有一种方法可以用不睡觉的异步事物来表达它吗?
让我们考虑如何将两个聚合函数应用到同一个迭代器中,我们只能一次使用它。最初的尝试(为了简洁起见,硬编码
sum
和max
,但可以简单地概括为任意数量的聚合函数)可能如下所示:
def max_and_sum_buffer(it):
content = list(it)
p = sum(content)
m = max(content)
return p, m
def aggregate(iterator, *functions):
first = next(iterator)
result = [first] * len(functions)
for item in iterator:
for i, f in enumerate(functions):
result[i] = f((result[i], item))
return result
这种实现的缺点是,它一次将所有生成的元素存储在内存中,尽管这两个函数都完全能够进行流处理。问题预测到了这种情况,并明确要求在不缓冲迭代器输出的情况下生成结果。有可能这样做吗
串行执行:itertools.tee
这当然是可能的。毕竟,Python迭代器是可挂起的,所以每个迭代器都已经能够挂起自己了。提供一个适配器将迭代器拆分为两个提供相同内容的新迭代器有多难?事实上,这正是的描述,它似乎非常适合并行迭代:
def max_and_sum_tee(it):
it1, it2 = itertools.tee(it)
p = sum(it1) # XXX
m = max(it2)
return p, m
上面给出的结果是正确的,但不是我们希望的那样。问题是我们没有并行迭代。聚合函数,如sum
和max
从不挂起-每个函数都坚持在生成结果之前使用所有迭代器内容。因此sum
将在max
有机会运行之前耗尽it1
。将it1
的元素耗尽,而将it2
单独保留,将导致这些元素在两个迭代器之间共享的内部FIFO中累积。这在这里是不可避免的-因为max(it2)
必须看到相同的元素,tee
别无选择,只能累积它们。(有关tee
的更多有趣细节,请参阅)
换句话说,这个实现和第一个实现之间没有区别,除了第一个实现至少使缓冲显式化。为了消除缓冲,sum
和max
必须并行运行,而不是一个接一个地运行
线程:concurrent.futures
让我们看看如果我们在单独的线程中运行聚合函数,仍然使用tee
复制原始迭代器会发生什么:
def max_and_sum_threads_simple(it):
it1, it2 = itertools.tee(it)
with concurrent.futures.ThreadPoolExecutor(2) as executor:
sum_future = executor.submit(lambda: sum(it1))
max_future = executor.submit(lambda: max(it2))
return sum_future.result(), max_future.result()
现在sum
和max
实际上是并行运行的(只要允许),线程由优秀的模块管理。但是,它有一个致命的缺陷:tee
不缓冲数据,sum
和max
必须以完全相同的速率处理它们的项。如果其中一个比另一个快一点,它们将分开漂移,并且tee
将缓冲所有中间元素。由于无法预测每种缓存的运行速度,因此缓冲量既不可预测,又具有最糟糕的缓冲效果
为确保不发生缓冲,必须使用自定义生成器替换tee
,该生成器不缓冲任何内容并阻塞,直到所有使用者都观察到上一个值,然后再继续下一个值。与以前一样,每个使用者在其自己的线程中运行,但现在调用线程正忙于运行生产者,这是一个循环,实际上在源迭代器上迭代,并发出新值可用的信号。下面是一个实现:
def max_and_sum_threads(it):
STOP = object()
next_val = None
consumed = threading.Barrier(2 + 1) # 2 consumers + 1 producer
val_id = 0
got_val = threading.Condition()
def send(val):
nonlocal next_val, val_id
consumed.wait()
with got_val:
next_val = val
val_id += 1
got_val.notify_all()
def produce():
for elem in it:
send(elem)
send(STOP)
def consume():
last_val_id = -1
while True:
consumed.wait()
with got_val:
got_val.wait_for(lambda: val_id != last_val_id)
if next_val is STOP:
return
yield next_val
last_val_id = val_id
with concurrent.futures.ThreadPoolExecutor(2) as executor:
sum_future = executor.submit(lambda: sum(consume()))
max_future = executor.submit(lambda: max(consume()))
produce()
return sum_future.result(), max_future.result()
对于概念上如此简单的东西,这是相当多的代码,但对于正确的操作是必需的
product()。它使用一个在Python 3.2中添加的方便的同步原语,在next\val
中用新值覆盖之前,等待所有使用者都使用旧值。一旦新值实际就绪,将广播一个新值consume()
是一个生成器,它在生成的值到达时传输这些值,直到检测到STOP
。通过在循环中创建使用者,并在创建屏障时调整其数量,可以将代码概括为并行运行任意数量的聚合函数
这种实现的缺点是,它需要创建线程(可能通过使线程池成为全局线程来缓解),并在每次迭代过程中进行大量非常仔细的同步。这种同步会破坏性能-此版本几乎比单线程tee
慢2000倍,比简单但不确定的线程版本慢475倍
不过,只要使用线程,就无法避免某种形式的同步。要完全消除同步,我们必须放弃线程,转而使用协作多任务。问题是是否可以暂停执行普通同步函数,如sum
和max
,以便在它们之间切换
纤维:绿色
事实证明,第三方扩展模块正是实现了这一点。Greenlets是一个轻量级微线程的实现,可以在彼此之间显式切换。这有点像Python生成器,它使用yield
来挂起,只是greenlets提供了一种更灵活的挂起机制,允许用户选择挂起的对象
这使得移植线程版本的max\u和\u sum
t相当容易
async def asum(it):
s = 0
async for elem in it:
s += elem
return s
async def amax(it):
NONE_YET = object()
largest = NONE_YET
async for elem in it:
if largest is NONE_YET or elem > largest:
largest = elem
if largest is NONE_YET:
raise ValueError("amax() arg is an empty sequence")
return largest
# or, using https://github.com/vxgmichel/aiostream
#
#from aiostream.stream import accumulate
#def asum(it):
# return accumulate(it, initializer=0)
#def amax(it):
# return accumulate(it, max)
async def max_and_sum_asyncio(it):
loop = asyncio.get_event_loop()
STOP = object()
next_val = loop.create_future()
consumed = loop.create_future()
used_cnt = 2 # number of consumers
async def produce():
for elem in it:
next_val.set_result(elem)
await consumed
next_val.set_result(STOP)
async def consume():
nonlocal next_val, consumed, used_cnt
while True:
val = await next_val
if val is STOP:
return
yield val
used_cnt -= 1
if not used_cnt:
consumed.set_result(None)
consumed = loop.create_future()
next_val = loop.create_future()
used_cnt = 2
else:
await consumed
s, m, _ = await asyncio.gather(asum(consume()), amax(consume()),
produce())
return s, m
def max_and_sum_asyncio_sync(it):
# trivially instantiate the coroutine and execute it in the
# default event loop
coro = max_and_sum_asyncio(it)
return asyncio.get_event_loop().run_until_complete(coro)
def aggregate(iterator, *functions):
first = next(iterator)
result = [first] * len(functions)
for item in iterator:
for i, f in enumerate(functions):
result[i] = f((result[i], item))
return result