Python並行プログラミングのためのAsyncio

I / O操作を処理する場合、マルチスレッドの使用効率は通常のシングルスレッドに比べて大幅に向上します。この場合、なぜAsyncioが必要なのか考えているかもしれません。

  • たとえば、マルチスレッド操作のプロセスは中断されやすいため、競合状態が発生する可能性があります。
  • もう1つの例は、スレッドの切り替え自体に一定の損失があり、スレッドの数を無制限に増やすことはできないため、I / O操作が非常に重い場合、マルチスレッドは高効率と高品質の要件を満たさない可能性があります。

Asyncioが生まれたのはこれらの問題を解決することです。

Asyncioとは

同期VS非同期

まず、同期(同期)と非同期(非同期)の概念を区別しましょう。

  • いわゆる同期とは、次々に操作を実行することを指し、次の操作は、前の操作が完了するのを待ってから実行する必要があります。
  • 非同期とは、異なる操作を交互に実行できることを意味します。操作の1つがブロックされた場合、プログラムは待機しませんが、実行可能操作を見つけて実行を続行します。

簡単な例を挙げると、上司から、この四半期のレポートを作成してメールで送信するように求められます。

  • Sync方式に従う場合は、最初にこの四半期のデータをソフトウェアに入力し、次に5分間待ってから、レポートにデータが生成されたことが示された後、彼に電子メールを送信します。
  • ただし、Async方式に従う場合は、この四半期のデータの入力が完了した後にメールの作成を開始します。レポートがレポートの生成を示したら、電子メールを一時停止し、最初にレポートを確認し、確認後、送信されるまで電子メールの書き込みを続けます。

Asyncioのしくみ

SyncとAsyncを理解した後、今日のトピックに戻りますが、Asyncioとは正確には何ですか?

実際、Asyncioは、他のPythonプログラムと同様に、シングルスレッドです。メインスレッドは1つだけですが、複数の異なるタスクを実行できます。ここでのタスクは、特別な将来のオブジェクトです。これらのさまざまなタスクは、イベントループと呼ばれるオブジェクトによって制御されます。ここでのタスクを、マルチスレッドバージョンの複数のスレッドと比較できます。

この問題の説明を簡単にするために、タスクには2つの状態しかないと想定できます。1つは準備完了状態で、もう1つは待機状態です。いわゆる準備完了状態とは、タスクが現在アイドル状態であるが、いつでも実行できる状態であることを意味します。待機状態とは、タスクが実行されているが、I / O操作などの外部操作の完了を待機していることを指します。

この場合、イベントループは、これら2つの状態に対応する2つのタスクリストを維持し、準備完了状態のタスクを選択します(待機時間の長さ、占有されているリソースなどに関連して、どのタスクが具体的に選択されますか)。タスクが制御をイベントループに戻すまで実行します。

タスクが制御をイベントループに戻すと、イベントループは、タスクが完了したかどうかに応じて、タスクを準備状態または待機状態のリストに配置し、待機状態リスト内のタスクをトラバースして、タスクが完了したかどうかを確認します。

  • 完了したら、準備完了ステータスのリストに入れます。
  • 完了していない場合は、引き続き待機状態リストに入れられます。

元々準備ステータスリストにあったタスクの位置は、まだ実行されていないため変更されません。

このようにして、すべてのタスクが適切なリストに再配置されると、ループの新しいラウンドが始まります。イベントループは、準備完了状態のリストからタスクを選択し続け、それを実行します...など。タスクが完了しました。

Asyncioの場合、そのタスクは操作中にいくつかの外部要因によって中断されないため、Asyncioでの操作には競合状態がないため、スレッドセーフの問題を心配する必要はありません。

Asyncioの使用法

Asyncioの原則について説明した後、特定のコードと組み合わせてその使用法を見てみましょう。前のレッスンでWebサイトのコンテンツをダウンロードする例を見てみましょう。Asyncioを使用して、次のコードに配置します(例外処理の一部の操作は省略されています)。一緒に見てみましょう。


import asyncio
import aiohttp
import time

async def download_one(url):
    async with aiohttp.ClientSession() as session:
        async with session.get(url) as resp:
            print('Read {} from {}'.format(resp.content_length, url))

async def download_all(sites):
    tasks = [asyncio.create_task(download_one(site)) for site in sites]
    await asyncio.gather(*tasks)

def main():
    sites = [
        'https://en.wikipedia.org/wiki/Portal:Arts',
        'https://en.wikipedia.org/wiki/Portal:History',
        'https://en.wikipedia.org/wiki/Portal:Society',
        'https://en.wikipedia.org/wiki/Portal:Biography',
        'https://en.wikipedia.org/wiki/Portal:Mathematics',
        'https://en.wikipedia.org/wiki/Portal:Technology',
        'https://en.wikipedia.org/wiki/Portal:Geography',
        'https://en.wikipedia.org/wiki/Portal:Science',
        'https://en.wikipedia.org/wiki/Computer_science',
        'https://en.wikipedia.org/wiki/Python_(programming_language)',
        'https://en.wikipedia.org/wiki/Java_(programming_language)',
        'https://en.wikipedia.org/wiki/PHP',
        'https://en.wikipedia.org/wiki/Node.js',
        'https://en.wikipedia.org/wiki/The_C_Programming_Language',
        'https://en.wikipedia.org/wiki/Go_(programming_language)'
    ]
    start_time = time.perf_counter()
    asyncio.run(download_all(sites))
    end_time = time.perf_counter()
    print('Download {} sites in {} seconds'.format(len(sites), end_time - start_time))
    
if __name__ == '__main__':
    main()

## 输出
Read 63153 from https://en.wikipedia.org/wiki/Java_(programming_language)
Read 31461 from https://en.wikipedia.org/wiki/Portal:Society
Read 23965 from https://en.wikipedia.org/wiki/Portal:Biography
Read 36312 from https://en.wikipedia.org/wiki/Portal:History
Read 25203 from https://en.wikipedia.org/wiki/Portal:Arts
Read 15160 from https://en.wikipedia.org/wiki/The_C_Programming_Language
Read 28749 from https://en.wikipedia.org/wiki/Portal:Mathematics
Read 29587 from https://en.wikipedia.org/wiki/Portal:Technology
Read 79318 from https://en.wikipedia.org/wiki/PHP
Read 30298 from https://en.wikipedia.org/wiki/Portal:Geography
Read 73914 from https://en.wikipedia.org/wiki/Python_(programming_language)
Read 62218 from https://en.wikipedia.org/wiki/Go_(programming_language)
Read 22318 from https://en.wikipedia.org/wiki/Portal:Science
Read 36800 from https://en.wikipedia.org/wiki/Node.js
Read 67028 from https://en.wikipedia.org/wiki/Computer_science
Download 15 sites in 0.062144195078872144 seconds

ここでのAsyncおよびawaitキーワードは、Asyncioの最新の表現であり、このステートメント/関数が非ブロックであることを示しています。これは、前述のイベントループの概念に対応しています。タスク実行プロセスを待機する必要がある場合は、待機状態リストに入れてから、準備状態リストでタスクの実行を続行します。

main関数のasyncio.run(coro)は、Asyncioのルート呼び出しです。これは、イベントループを取得し、入力coroを終了するまで実行し、最後にイベントループを閉じることを意味します。実際、asyncio.run()はPython 3.7以降でのみ導入されました。これは、古いバージョンの次のステートメントと同等です。


loop = asyncio.get_event_loop()
try:
    loop.run_until_complete(coro)
finally:
    loop.close()

関数download_all()のAsyncioバージョンに関しては、以前のマルチスレッドバージョンとは大きな違いがあります。


tasks = [asyncio.create_task(download_one(site)) for site in sites]
await asyncio.gather(*task)

ここで、asyncio.create_task(coro)は、入力コルーチンcoroのタスクを作成し、その実行を調整して、このタスクオブジェクトを返すことを意味します。この関数はPython3.7以降でも新しく追加されました。以前のバージョンの場合は、asyncio.ensure_future(coro)を使用して同等に置き換えることができます。ご覧のとおり、ここでは、Webサイトのダウンロードごとに対応するタスクを作成しました。

さらに下を見ると、asyncio.gather(* aws、loop = None、return_exception = False)は、AWSシーケンスのすべてのタスクがイベントループで実行されることを意味します。もちろん、例で使用されている関数に加えて、Asyncioは他の多くの使用法も提供しています。対応するドキュメントを確認して、理解することができます。

最後に、最終的な出力結果を見てみましょう。0.06秒しかかかりません。以前のマルチスレッドバージョンと比較すると、効率はより高いレベルであり、その利点を完全に反映していると言えます。

Asyncioに欠陥がありますか?

多くのことを学んだ後、Asyncioの力に気づきましたが、どのソリューションも完全ではなく、特定の制限があることを明確にする必要があります。Asyncioについても同じことが言えます。

実際の作業で、Asyncioをうまく利用したい場合、特にその強力な機能を十分に活用したい場合は、多くの場合、対応するPythonライブラリのサポートが必要です。前回のレッスンのマルチスレッドプログラミングでは、requestsライブラリを使用していましたが、今日は使用していません。代わりに、aiohttpライブラリを使用しました。その理由は、requestsライブラリがAsyncioと互換性がないためです。 、ただし、aiohttpライブラリには互換性があります。

Asyncioソフトウェアライブラリの互換性の問題は、Python3の初期には大きな問題でしたが、技術の開発により、この問題は徐々に解決されています。

さらに、Asyncioを使用する場合、タスクスケジューリングの自律性が高いため、コードを記述する際により多くの注意を払う必要があります。そうしないと、間違いを犯しやすくなります。

たとえば、一連の操作を待つ必要がある場合は、asyncio.gather()を使用する必要があります。それが単一の未来である場合は、asyncio.wait()を使用するだけです。それで、あなたの将来のために、あなたはそれをrun_until_complete()またはrun_forever()にしたいですか?これらはすべて、特定の問題に直面したときに考慮する必要があることです。

マルチスレッドまたは非同期

無意識のうちに、私たちは並行プログラミングの両方の方法を学びました。ただし、実際的な問題が発生した場合、マルチスレッドとAsyncioのどちらを選択しますか?

一般に、次の擬似コード仕様に従うことができます。


if io_bound:
    if io_slow:
        print('Use Asyncio')
    else:
        print('Use multi-threading')
else if cpu_bound:
    print('Use multi-processing')
  • それがI / Oバウンドであり、I / O操作が非常に遅く、多くのタスク/スレッドを協調して実装する必要がある場合は、Asyncioの方が適切です。
  • I / Oバウンドであるが、I / O操作が非常に高速で、必要なタスク/スレッドの数が限られている場合は、マルチスレッドで問題ありません。
  • CPUにバインドされている場合は、プログラムの効率を向上させるために複数のプロセスを使用する必要があります。

おすすめ

転載: blog.csdn.net/qq_41485273/article/details/114178427