web-dev-qa-db-ja.com

Python asyncio?で並行性を制限するには?

ダウンロードするリンクの束があり、各リンクのダウンロードに異なる時間がかかると仮定しましょう。また、最大3つの接続のみを使用してダウンロードできます。ここで、asyncioを使用してこれを効率的に行うようにします。

私が達成しようとしていることは次のとおりです。いつでも、少なくとも3つのダウンロードが実行されていることを確認してください。

Connection 1: 1---------7---9---
Connection 2: 2---4----6-----
Connection 3: 3-----5---8-----

数字はダウンロードリンクを表し、ハイフンはダウンロードの待機を表します。

ここに私が今使っているコードがあります

from random import randint
import asyncio

count = 0


async def download(code, permit_download, no_concurrent, downloading_event):
    global count
    downloading_event.set()
    wait_time = randint(1, 3)
    print('downloading {} will take {} second(s)'.format(code, wait_time))
    await asyncio.sleep(wait_time)  # I/O, context will switch to main function
    print('downloaded {}'.format(code))
    count -= 1
    if count < no_concurrent and not permit_download.is_set():
        permit_download.set()


async def main(loop):
    global count
    permit_download = asyncio.Event()
    permit_download.set()
    downloading_event = asyncio.Event()
    no_concurrent = 3
    i = 0
    while i < 9:
        if permit_download.is_set():
            count += 1
            if count >= no_concurrent:
                permit_download.clear()
            loop.create_task(download(i, permit_download, no_concurrent, downloading_event))
            await downloading_event.wait()  # To force context to switch to download function
            downloading_event.clear()
            i += 1
        else:
            await permit_download.wait()
    await asyncio.sleep(9)

if __name__ == '__main__':
    loop = asyncio.get_event_loop()
    try:
        loop.run_until_complete(main(loop))
    finally:
        loop.close()

そして、出力は予想通りです。

downloading 0 will take 2 second(s)
downloading 1 will take 3 second(s)
downloading 2 will take 1 second(s)
downloaded 2
downloading 3 will take 2 second(s)
downloaded 0
downloading 4 will take 3 second(s)
downloaded 1
downloaded 3
downloading 5 will take 2 second(s)
downloading 6 will take 2 second(s)
downloaded 5
downloaded 6
downloaded 4
downloading 7 will take 1 second(s)
downloading 8 will take 1 second(s)
downloaded 7
downloaded 8

しかし、ここに私の質問があります:

  1. 現時点では、ダウンロードが完了するまでメイン機能を実行し続けるために9秒間待つだけです。メイン機能を終了する前に、最後のダウンロードが完了するのを待つ効率的な方法はありますか? (asyncio.waitがあることは知っていますが、動作するにはすべてのタスク参照を保存する必要があります)

  2. この種のタスクを行う優れたライブラリは何ですか? javascriptには多くの非同期ライブラリがありますが、Pythonはどうですか?

編集:2.一般的な非同期パターンを処理する優れたライブラリとは何ですか? ( https://www.npmjs.com/package/async のようなもの)

13
Shridharshan

基本的に、ダウンロードタスクの固定サイズpoolが必要です。 asyncioにはそのような機能が付属していませんが、簡単に作成できます。タスクのセットを保持し、制限を超えて成長しないようにするだけです。質問はそのルートを下るのを嫌がっていると述べていますが、コードはより洗練されたものになります。

_async def download(code):
    wait_time = randint(1, 3)
    print('downloading {} will take {} second(s)'.format(code, wait_time))
    await asyncio.sleep(wait_time)  # I/O, context will switch to main function
    print('downloaded {}'.format(code))

async def main(loop):
    no_concurrent = 3
    dltasks = set()
    i = 0
    while i < 9:
        if len(dltasks) >= no_concurrent:
            # Wait for some download to finish before adding a new one
            _done, dltasks = await asyncio.wait(
                dltasks, return_when=asyncio.FIRST_COMPLETED)
        dltasks.add(loop.create_task(download(i)))
        i += 1
    # Wait for the remaining downloads to finish
    await asyncio.wait(dltasks)
_

別の方法は、固定サイズのスレッドプールのように、ダウンロードを行う固定数のコルーチンを作成し、_asyncio.Queue_を使用してそれらを機能させることです。これにより、ダウンロード数を手動で制限する必要がなくなります。ダウンロード数は、download()を呼び出すコルーチンの数によって自動的に制限されます。

_# download() defined as above

async def download_from(q):
    while True:
        code = await q.get()
        if code is None:
            # pass on the Word that we're done, and exit
            await q.put(None)
            break
        await download(code)

async def main(loop):
    q = asyncio.Queue()
    dltasks = [loop.create_task(download_from(q)) for _ in range(3)]
    i = 0
    while i < 9:
        await q.put(i)
        i += 1
    # Inform the consumers there is no more work.
    await q.put(None)
    await asyncio.wait(dltasks)
_

他の質問に関しては、明らかな選択肢は aiohttp です。

15
user4815162342

間違っていなければ、 asyncio.Semaphore を検索しています。使用例:

import asyncio
from random import randint


async def download(code):
    wait_time = randint(1, 3)
    print('downloading {} will take {} second(s)'.format(code, wait_time))
    await asyncio.sleep(wait_time)  # I/O, context will switch to main function
    print('downloaded {}'.format(code))


sem = asyncio.Semaphore(3)


async def safe_download(i):
    async with sem:  # semaphore limits num of simultaneous downloads
        return await download(i)


async def main():
    tasks = [
        asyncio.ensure_future(safe_download(i))  # creating task starts coroutine
        for i
        in range(9)
    ]
    await asyncio.gather(*tasks)  # await moment all downloads done


if __name__ ==  '__main__':
    loop = asyncio.get_event_loop()
    try:
        loop.run_until_complete(main())
    finally:
        loop.run_until_complete(loop.shutdown_asyncgens())
        loop.close()

出力:

downloading 0 will take 3 second(s)
downloading 1 will take 3 second(s)
downloading 2 will take 1 second(s)
downloaded 2
downloading 3 will take 3 second(s)
downloaded 1
downloaded 0
downloading 4 will take 2 second(s)
downloading 5 will take 1 second(s)
downloaded 5
downloaded 3
downloading 6 will take 3 second(s)
downloading 7 will take 1 second(s)
downloaded 4
downloading 8 will take 2 second(s)
downloaded 7
downloaded 8
downloaded 6

aiohttpを使用した非同期ダウンロードの例は here にあります。

29

Asyncio-poolライブラリは、まさに必要なことを行います。

https://pypi.org/project/asyncio-pool/


LIST_OF_URLS = ("http://www.google.com, ......)

pool = AioPool(size=3)
await pool.map(your_download_coroutine, LIST_OF_URLS)
1
MadeR