Python,如何制作异步数据生成器?
我有一个程序可以加载数据并对其进行处理。加载和处理都需要时间,我希望并行进行 以下是我的程序的同步版本,其中加载和处理是按顺序完成的,为了示例,这里是一些简单的操作:Python,如何制作异步数据生成器?,python,python-3.x,asynchronous,Python,Python 3.x,Asynchronous,我有一个程序可以加载数据并对其进行处理。加载和处理都需要时间,我希望并行进行 以下是我的程序的同步版本,其中加载和处理是按顺序完成的,为了示例,这里是一些简单的操作: import time def data_loader(): for i in range(4): time.sleep(1) # Simulated loading time yield i def main(): start = time.time() for da
import time
def data_loader():
for i in range(4):
time.sleep(1) # Simulated loading time
yield i
def main():
start = time.time()
for data in data_loader():
time.sleep(1) # Simulated processing time
processed_data = -data*2
print(f'At t={time.time()-start:.3g}, processed data {data} into {processed_data}')
if __name__ == '__main__':
main()
当我运行此命令时,我会得到以下输出:
At t=2.01, processed data 0 into 0
At t=4.01, processed data 1 into -2
At t=6.02, processed data 2 into -4
At t=8.02, processed data 3 into -6
循环每2秒运行一次,1秒用于加载,1秒用于处理
现在,我想制作一个异步版本,其中加载和处理是同时进行的,这样在处理器处理数据时,加载程序就可以准备好下一个数据。然后,打印第一条语句需要2秒,之后的每条语句需要1秒。预期产出将类似于:
At t=2.01, processed data 0 into 0
At t=3.01, processed data 1 into -2
At t=4.02, processed data 2 into -4
At t=5.02, processed data 3 into -6
理想情况下,只有主函数的内容需要更改,因为数据加载器代码不应该关心它可能以异步方式使用。模块的实用程序可能是您想要的
import time
import multiprocessing
def data_loader():
for i in range(4):
time.sleep(1) # Simulated loading time
yield i
def process_item(item):
time.sleep(1) # Simulated processing time
return (item, -item*2) # Return the original too.
def main():
start = time.time()
with multiprocessing.Pool() as p:
data_iterator = data_loader()
for (data, processed_data) in p.imap(process_item, data_iterator):
print(f'At t={time.time()-start:.3g}, processed data {data} into {processed_data}')
if __name__ == '__main__':
main()
这个输出
At t=2.03, processed data 0 into 0
At t=3.03, processed data 1 into -2
At t=4.04, processed data 2 into -4
At t=5.04, processed data 3 into -6
根据您的要求,您可能会发现.imap_无序的更快,而且值得一提的是,有一个基于线程的版本的池可用作multiprocessing.dummy.Pool–如果您的数据很大,这可能有助于避免IPC开销,您的处理不是用Python完成的,因此可以避免GIL。模块的实用程序可能就是您想要的
import time
import multiprocessing
def data_loader():
for i in range(4):
time.sleep(1) # Simulated loading time
yield i
def process_item(item):
time.sleep(1) # Simulated processing time
return (item, -item*2) # Return the original too.
def main():
start = time.time()
with multiprocessing.Pool() as p:
data_iterator = data_loader()
for (data, processed_data) in p.imap(process_item, data_iterator):
print(f'At t={time.time()-start:.3g}, processed data {data} into {processed_data}')
if __name__ == '__main__':
main()
这个输出
At t=2.03, processed data 0 into 0
At t=3.03, processed data 1 into -2
At t=4.04, processed data 2 into -4
At t=5.04, processed data 3 into -6
根据您的要求,您可能会发现.imap_无序的更快,而且值得一提的是,有一个基于线程的版本的池可用作multiprocessing.dummy.Pool–如果您的数据很大,这可能有助于避免IPC开销,您的处理不是用Python完成的,因此可以避免GIL。问题的关键在于数据的实际处理。我不知道您在实际程序中对数据做什么,但使用异步编程必须是异步操作。如果您正在执行活动的、阻塞CPU限制的处理,那么您最好将负载转移到一个单独的进程,以便能够使用多个CPU内核并同时执行任务。如果数据的实际处理实际上只是一些异步服务的消耗,那么它可以非常有效地包装在单个异步并发线程中 在您的示例中,您使用time.sleep来模拟处理过程。由于该示例操作可以通过使用asyncio.sleep异步完成,因此转换很简单:
import itertools
import asyncio
async def data_loader():
for i in itertools.count(0):
await asyncio.sleep(1) # Simulated loading time
yield i
async def process(data):
await asyncio.sleep(1) # Simulated processing time
processed_data = -data*2
print(f'At t={loop.time()-start:.3g}, processed data {data} into {processed_data}')
async def main():
tasks = []
async for data in data_loader():
tasks.append(loop.create_task(process(data)))
await asyncio.wait(tasks) # wait for all remaining tasks
if __name__ == '__main__':
loop = asyncio.get_event_loop()
start = loop.time()
loop.run_until_complete(main())
loop.close()
正如您所期望的,结果如下:
At t=2, processed data 0 into 0
At t=3, processed data 1 into -2
At t=4, processed data 2 into -4
...
请记住,它之所以有效,是因为time.sleep有一个异步替代方案,即asyncio.sleep。检查您正在使用的操作,看看它是否可以以异步形式写入。问题的关键在于数据的实际处理。我不知道您在实际程序中对数据做什么,但使用异步编程必须是异步操作。如果您正在执行活动的、阻塞CPU限制的处理,那么您最好将负载转移到一个单独的进程,以便能够使用多个CPU内核并同时执行任务。如果数据的实际处理实际上只是一些异步服务的消耗,那么它可以非常有效地包装在单个异步并发线程中 在您的示例中,您使用time.sleep来模拟处理过程。由于该示例操作可以通过使用asyncio.sleep异步完成,因此转换很简单:
import itertools
import asyncio
async def data_loader():
for i in itertools.count(0):
await asyncio.sleep(1) # Simulated loading time
yield i
async def process(data):
await asyncio.sleep(1) # Simulated processing time
processed_data = -data*2
print(f'At t={loop.time()-start:.3g}, processed data {data} into {processed_data}')
async def main():
tasks = []
async for data in data_loader():
tasks.append(loop.create_task(process(data)))
await asyncio.wait(tasks) # wait for all remaining tasks
if __name__ == '__main__':
loop = asyncio.get_event_loop()
start = loop.time()
loop.run_until_complete(main())
loop.close()
正如您所期望的,结果如下:
At t=2, processed data 0 into 0
At t=3, processed data 1 into -2
At t=4, processed data 2 into -4
...
请记住,它之所以有效,是因为time.sleep有一个异步替代方案,即asyncio.sleep。检查您正在使用的操作,看看它是否可以以异步形式写入。这里有一个解决方案,允许您使用iter\u异步函数包装数据加载器。现在它解决了这个问题。但是请注意,仍然存在一个问题,即如果dataloader比处理循环快,那么队列将无限增长。如果队列变大,则可以通过在异步队列管理器中添加等待来轻松解决此问题,但遗憾的是,Mac不支持queue.qsize
import time
from multiprocessing import Queue, Process
class PoisonPill:
pass
def _async_queue_manager(gen_func, queue: Queue):
for item in gen_func():
queue.put(item)
queue.put(PoisonPill)
def iter_asynchronously(gen_func):
""" Given a generator function, make it asynchonous. """
q = Queue()
p = Process(target=_async_queue_manager, args=(gen_func, q))
p.start()
while True:
item = q.get()
if item is PoisonPill:
break
else:
yield item
def data_loader():
for i in range(4):
time.sleep(1) # Simulated loading time
yield i
def main():
start = time.time()
for data in iter_asynchronously(data_loader):
time.sleep(1) # Simulated processing time
processed_data = -data*2
print(f'At t={time.time()-start:.3g}, processed data {data} into {processed_data}')
if __name__ == '__main__':
main()
现在输出符合要求:
At t=2.03, processed data 0 into 0
At t=3.03, processed data 1 into -2
At t=4.04, processed data 2 into -4
At t=5.04, processed data 3 into -6
下面是一个解决方案,它允许您使用iter\u异步函数包装数据加载器。现在它解决了这个问题。但是请注意,仍然存在一个问题,即如果dataloader比处理循环快,那么队列将无限增长。如果队列变大,则可以通过在异步队列管理器中添加等待来轻松解决此问题,但遗憾的是,Mac不支持queue.qsize
import time
from multiprocessing import Queue, Process
class PoisonPill:
pass
def _async_queue_manager(gen_func, queue: Queue):
for item in gen_func():
queue.put(item)
queue.put(PoisonPill)
def iter_asynchronously(gen_func):
""" Given a generator function, make it asynchonous. """
q = Queue()
p = Process(target=_async_queue_manager, args=(gen_func, q))
p.start()
while True:
item = q.get()
if item is PoisonPill:
break
else:
yield item
def data_loader():
for i in range(4):
time.sleep(1) # Simulated loading time
yield i
def main():
start = time.time()
for data in iter_asynchronously(data_loader):
time.sleep(1) # Simulated processing time
processed_data = -data*2
print(f'At t={time.time()-start:.3g}, processed data {data} into {processed_data}')
if __name__ == '__main__':
main()
现在输出符合要求:
At t=2.03, processed data 0 into 0
At t=3.03, processed data 1 into -2
At t=4.04, processed data 2 into -4
At t=5.04, processed data 3 into -6
谢谢你的回答。为了简单起见,我忽略了这一点,但是如果处理器是有状态的呢?i、 e.我们现在可以这样做:在循环之前,processed_data=-data*2-processed_data,processed_data=0?如果有一个通用的生成器包装器,我们就可以使用它使生成器异步
无法使用multiprocessing.Value在进程之间设置状态。但是,如果您的状态依赖于迄今为止已处理的值,那么无论如何您都无法并行处理新值,因此您甚至不需要进行多重处理。谢谢您给出了这个很好的答案。为了简单起见,我忽略了这一点,但是如果处理器是有状态的呢?i、 e.我们现在可以这样做:在循环之前,processed_data=-data*2-processed_data,processed_data=0?如果有一个通用的生成器包装器,我们就可以使用它使生成器异步。您可以使用multiprocessing.Value在进程之间设置状态。但是,如果您的状态依赖于迄今为止已处理的值,则无论如何都无法并行处理新值,因此您甚至不需要多处理。谢谢。在我的应用程序中,加载和处理都是阻塞的、CPU限制的操作,所以我想这种方法对我不起作用,对吗?@Peter由于全局解释器锁GIL,python进程一次只能运行一段代码。也就是说,您可能对流程的性质有误解。特别是加载部分几乎不是CPU限制的操作,可能与数据输入/输出有关,数据输入/输出根本不使用CPU,可以以非阻塞方式写入;你确定吗?您能更详细地描述一下您的加载/处理部分吗,最好是代码?谢谢。在我的应用程序中,加载和处理都是阻塞的、CPU限制的操作,所以我想这种方法对我不起作用,对吗?@Peter由于全局解释器锁GIL,python进程一次只能运行一段代码。也就是说,您可能对流程的性质有误解。特别是加载部分几乎不是CPU限制的操作,可能与数据输入/输出有关,数据输入/输出根本不使用CPU,可以以非阻塞方式写入;你确定吗?您能否更详细地描述您的加载/处理部件,最好是代码?