web-dev-qa-db-ja.com

プロデューサーとコンシューマーのフローにasyncio.Queueを使用する

プロデューサーとコンシューマーの両方が同時に独立して動作する特定のプロデューサー-コンシューマーパターンに_asyncio.Queue_を使用する方法について混乱しています。

最初に、 _asyncio.Queue_のドキュメント の結果に密接に続くこの例を考えてみましょう。

_import asyncio
import random
import time

async def worker(name, queue):
    while True:
        sleep_for = await queue.get()
        await asyncio.sleep(sleep_for)
        queue.task_done()
        print(f'{name} has slept for {sleep_for:0.2f} seconds')

async def main(n):
    queue = asyncio.Queue()
    total_sleep_time = 0
    for _ in range(20):
        sleep_for = random.uniform(0.05, 1.0)
        total_sleep_time += sleep_for
        queue.put_nowait(sleep_for)
    tasks = []
    for i in range(n):
        task = asyncio.create_task(worker(f'worker-{i}', queue))
        tasks.append(task)
    started_at = time.monotonic()
    await queue.join()
    total_slept_for = time.monotonic() - started_at
    for task in tasks:
        task.cancel()
    # Wait until all worker tasks are cancelled.
    await asyncio.gather(*tasks, return_exceptions=True)
    print('====')
    print(f'3 workers slept in parallel for {total_slept_for:.2f} seconds')
    print(f'total expected sleep time: {total_sleep_time:.2f} seconds')

if __name__ == '__main__':
    import sys
    n = 3 if len(sys.argv) == 1 else sys.argv[1]
    asyncio.run(main())
_

このスクリプトに関する詳細がもう1つあります。アイテムは、従来のforループを介してqueue.put_nowait(sleep_for)で同期的にキューに入れられます。

私の目標は、async def worker()(またはconsumer())とasync def producer()を使用するスクリプトを作成することです。両方を同時に実行するようにスケジュールする必要があります。消費者のコルーチンは明示的に生産者に結び付けられたり、生産者から連鎖されたりすることはありません。

プロデューサーが、コンシューマー/ワーカーと同時にスケジュールできる独自のコルーチンになるように、上記のプログラムを変更するにはどうすればよいですか?


[〜#〜] pymotw [〜#〜] の2番目の例があります。プロデューサーは事前にコンシューマーの数を知る必要があり、Noneを使用して、生産が完了したことをコンシューマーに知らせます。

7
Brad Solomon

プロデューサーが、コンシューマー/ワーカーと同時にスケジュールできる独自のコルーチンになるように、上記のプログラムを変更するにはどうすればよいですか?

この例は、基本的なロジックを変更せずに一般化できます。

  • 挿入ループを別のプロデューサーコルーチンに移動します。
  • バックグラウンドでコンシューマーを起動し、生成されたアイテムを処理させます。
  • await producer()await gather(*producers)などと同様に、プロデューサーがawaitingして終了するのを待ちます。
  • すべてのプロデューサーが完了したら、await queue.join()で残りのプロデュースアイテムが処理されるのを待ちます
  • 消費者をキャンセルします。消費者はすべて、次のキューに登録されたアイテムが到着するのを待っています。

上記を実装する例を次に示します。

import asyncio, random, time

async def rnd_sleep(t):
    # sleep for T seconds on average
    await asyncio.sleep(t * random.random() * 2)

async def producer(queue):
    while True:
        token = random.random()
        print(f'produced {token}')
        if token < .05:
            break
        await queue.put(token)
        await rnd_sleep(.1)

async def consumer(queue):
    while True:
        token = await queue.get()
        await rnd_sleep(.3)
        queue.task_done()
        print(f'consumed {token}')

async def main():
    queue = asyncio.Queue()

    # fire up the both producers and consumers
    producers = [asyncio.create_task(producer(queue))
                 for _ in range(3)]
    consumers = [asyncio.create_task(consumer(queue))
                 for _ in range(10)]

    # with both producers and consumers running, wait for
    # the producers to finish
    await asyncio.gather(*producers)
    print('---- done producing')

    # wait for the remaining tasks to be processed
    await queue.join()

    # cancel the consumers, which are now idle
    for c in consumers:
        c.cancel()

asyncio.run(main())
12
user4815162342