Pythonの非同期IO:完全なチュートリアル

Pythonの非同期IO:完全なチュートリアル

非同期IOは、Python 3.4から3.7、およびhttps://twitter.com/1st1/status/1041855365745455104 [おそらくそれ以上]に急速に進化するPythonの専用サポートを受けた同時プログラミング設計です。

「並行性、並列処理、スレッド化、マルチプロセッシング。 すでに把握することはたくさんあります。 非同期IOはどこに収まりますか?」

このチュートリアルは、その質問に答えるのに役立つように構築されており、非同期IOに対するPythonのアプローチをよりしっかりと把握できます。

説明する内容:

  • 非同期IO(非同期IO):プログラミング言語のホスト全体に実装されている言語に依存しないパラダイム(モデル)

  • + async +/+ await + :コルーチンを定義するために使用される2つの新しいPythonキーワード

  • + asyncio + :コルーチンを実行および管理するための基盤とAPIを提供するPythonパッケージ

コルーチン(専用のジェネレーター関数)はPythonの非同期IOの中心であり、後で詳しく説明します。

注意:この記事では、 async IO という用語を使用して、非同期IOの言語に依存しない設計を示しますが、 `+ asyncio +`はPythonパッケージを指します。

始める前に、「+ asyncio +」とこのチュートリアルにある他のライブラリを使用するように設定されていることを確認する必要があります。

無料ボーナス: link:[5 Thoughts On Python Mastery]、Python開発者向けの無料コースで、Pythonのスキルを次のレベルに引き上げるのに必要なロードマップと考え方を示します。

環境を設定する

この記事全体を読むには、Python 3.7以上と、 `+ aiohttp `および ` aiofiles +`パッケージが必要です。

$ python3.7 -m venv ./py37async
$ source ./py37async/bin/activate  # Windows: .\py37async\Scripts\activate.bat
$ pip install --upgrade pip aiohttp aiofiles  # Optional: aiodns

Python 3.7のインストールおよび仮想環境のセットアップについては、https://realpython.com/installing-python/[Python 3 Installation&Setup Guide]またはhttps://realpython.com/python-virtual-environments-をご覧ください。 a-primer/[仮想環境入門]。

それでは、飛び込みましょう。

非同期IOの10,000フィートビュー

非同期IOは、実証済みの従兄弟であるマルチプロセッシングとスレッドよりも少し知られていないものです。 このセクションでは、非同期IOとは何か、それが周囲のランドスケープにどのように適合するかについて、より詳細に説明します。

非同期IOはどこに収まりますか?

並行性と並列性は広範に及ぶ主題であり、容易に理解することはできません。 この記事では非同期IOとPythonでの実装に焦点を当てていますが、非同期IOがより大きく、めまいがするパズルにどのように適合するかについてのコンテキストを得るために、非同期IOを対応するものと比較するのに少し時間をかける価値があります。

*並列*は、複数の操作を同時に実行することで構成されます。 *マルチプロセッシング*は、並列処理を実現する手段であり、コンピューターの中央処理装置(CPU、またはコア)にタスクを分散させる必要があります。 マルチプロセッシングは、CPUにバインドされたタスクに適しています。緊密にバインドされた「+ for +」ループと数学計算は、通常このカテゴリに分類されます。

*同時実行*は、並列処理よりも少し広い用語です。 複数のタスクが重複して実行できることを示しています。 (並行性は並列性を意味しないと言われています。)

*スレッド化*は、複数のhttps://en.wikipedia.org/wiki/Thread_(computing)[threads]が交互にタスクを実行する同時実行モデルです。 1つのプロセスに複数のスレッドを含めることができます。 Pythonはhttps://realpython.com/python-gil/[GIL]のおかげでスレッドと複雑な関係にありますが、それはこの記事の範囲外です。

スレッド化について知っておくべき重要なことは、IOにバインドされたタスクに適していることです。 CPUにバインドされたタスクは、コンピューターのコアが最初から最後まで継続的に懸命に動作することを特徴としていますが、IOにバインドされたジョブは、入力/出力の完了を待つことが多くなります。

上記を要約すると、並行処理には、マルチプロセッシング(CPUにバインドされたタスクに最適)とスレッド(IOにバインドされたタスクに適した)の両方が含まれます。 マルチプロセッシングは並列処理の一種であり、並列処理は特定のタイプ(サブセット)の並行処理です。 Python標準ライブラリは、「+ multiprocessing 」、「 threading 」、および「 concurrent.futures +」を通じて、長年にわたるhttps://docs.python.org/3/library/concurrency.html [これらの両方のサポート]を提供しています。パッケージ。

次は、新しいメンバーをミックスに追加します。 過去数年にわたって、標準ライブラリの「+ asyncio 」パッケージと新しい「 async 」および「 await +」言語キーワードによって有効化された別の設計が、CPythonにより包括的に組み込まれました。非同期IO。 明らかに、非同期IOは新しく考案された概念ではなく、https://gobyexample.com/goroutines [Go]、https://docs.microsoftなどの他の言語およびランタイム環境に存在するか、組み込まれています。 .com/en-us/dotnet/csharp/async [C#]、またはhttps://docs.scala-lang.org/sips/async.html[Scala]。

`+ asyncio +`パッケージは、https://docs.python.org/3/library/asyncio.html [並行コードを記述するライブラリ]としてPythonドキュメントから請求されます。 ただし、非同期IOはスレッドではなく、マルチプロセッシングでもありません。 これらの上に構築されていません。

実際、非同期IOはシングルスレッド、シングルプロセスの設計です。このチュートリアルでは、「協調マルチタスク」という用語を使用します。この用語は、このチュートリアルの終わりまでに詳しく説明します。 言い換えれば、非同期IOは、単一プロセスで単一スレッドを使用しているにもかかわらず、並行性の感覚を与えると言われています。 コルーチン(非同期IOの中心機能)は同時にスケジュールできますが、本質的に同時ではありません。

繰り返しますが、非同期IOは並行プログラミングのスタイルですが、並列処理ではありません。 マルチプロセッシングよりもスレッドと密接に連携していますが、これらの両方とはまったく異なり、並行処理の裏技のスタンドアロンメンバーです。

それはもう1つの用語を残します。 何かが*非同期*であるとはどういう意味ですか? これは厳密な定義ではありませんが、ここでの目的のために、2つのプロパティを考えることができます。

  • 非同期ルーチンは、最終結果を待機しながら「一時停止」し、その間に他のルーチンを実行できます。

  • 非同期コードは、上記のメカニズムにより、同時実行を促進します。 別の言い方をすれば、非同期コードは並行性のルックアンドフィールを提供します。

すべてをまとめた図を次に示します。 白の用語は概念を表し、緑の用語はそれらが実装または実行される方法を表します。

並行プログラミングモデルの比較については、これで終わりです。 このチュートリアルは、非同期IOであるサブコンポーネント、その使用方法、およびその周辺に出現したAPIに焦点を当てています。 スレッド化、マルチプロセッシング、非同期IOの徹底的な調査については、ここで一時停止し、ジムアンダーソンのhttps://realpython.com/python-concurrency/[Pythonでの並行性の概要]をご覧ください。 ジムは私よりもずっとおもしろくて、私よりも多くのミーティングに参加してブートしました。

非同期IOの説明

非同期IOは、最初は直感に反し、逆説的に見えるかもしれません。 並行コードを容易にするものは、単一のスレッドと単一のCPUコアをどのように使用しますか? 私は例を思い付くのが得意ではなかったので、ミゲル・グリンバーグの2017年のPyConトークから1つを言い換えたいと思います。

_ _ チェスマスターのジュディ・ポルガルは、複数のアマチュア選手を演じるチェスの展示会を主催しています。 彼女には展示を行う方法が2つあります。同期的と非同期的です。

仮定:

  • 24人の対戦相手

  • Juditは各チェスを5秒で移動させます

  • 相手はそれぞれ55秒で移動します

  • ゲームは平均30ペア移動(合計60移動)

同期バージョン:Juditは、ゲームが完了するまで一度に1つのゲームをプレイし、同時に2つのゲームをプレイすることはありません。 各ゲームには_(55 + 5) 30 == 1800_秒、つまり30分かかります。 展示会全体の所要時間は_24 30 == 720_分、または* 12時間*です。

非同期バージョン:Juditはテーブルからテーブルに移動し、テーブルごとに移動します。 彼女はテーブルを離れ、待機時間中に相手に次の動きをさせます。 24試合すべてで1回の移動には、Judit 24 5 == 120_秒、つまり2分かかります。 展示全体が_120 30 == 3600_秒、またはわずか* 1時間*に削減されました。 https://youtu.be/iG6fr81xHKA?t=4m29s [(ソース)] _ __

JuditPolgárは1人しかいません。JuditPolgárは両手のみで、自分で一度に1つだけ移動します。 ただし、非同期で再生すると、展示時間は12時間から1時間に短縮されます。 そのため、協調型マルチタスクは、プログラムのイベントループ(詳細は後述)が複数のタスクと通信して、それぞれが最適なタイミングで順番に実行できるようにするという凝った方法です。

非同期IOは、そうでなければ機能がブロックされる長い待機期間をとり、そのダウンタイム中に他の機能を実行できるようにします。 (ブロックする関数は、他の人が開始してから戻るまで実行することを効果的に禁止します。)

非同期IOは簡単ではありません

「できる限り非同期IOを使用してください。必要なときにスレッドを使用します。」真実は、耐久性のあるマルチスレッドコードの構築は難しく、エラーが発生しやすいということです。 非同期IOを使用すると、スレッド化された設計で発生する可能性のある潜在的なスピードバンプの一部が回避されます。

しかし、それはPythonの非同期IOが簡単だということではありません。 警告:表面レベルより少し下に行くと、非同期プログラミングも難しくなります! Pythonの非同期モデルは、コールバック、イベント、トランスポート、プロトコル、未来などの概念に基づいて構築されています。用語だけでは威圧できません。 そのAPIが絶えず変化しているという事実により、それは容易ではありません。

幸いなことに、「+ asyncio +」はその機能のほとんどが暫定的ではなくなるまで成熟しましたが、そのドキュメントは大幅なオーバーホールを受け、主題に関するいくつかの質の高いリソースも出現し始めています。

+ asyncio +`パッケージと `+ async +/+ await +

設計として非同期IOの背景がわかったところで、Pythonの実装を調べてみましょう。 Pythonの `+ asyncio `パッケージ(Python 3.4で導入)とその2つのキーワード、 ` async `と ` await +`は異なる目的を果たしますが、非同期コードの宣言、構築、実行、管理を支援するために一緒になります。

+ async +/`+ await +`構文とネイティブコルーチン

注意事項:インターネット上で読み上げる内容に注意してください。 Pythonの非同期IO APIは、Python 3.4からPython 3.7に急速に進化しました。 一部の古いパターンは使用されなくなり、最初は禁止されていたものが、新しい紹介を通じて許可されるようになりました。 私が知っているすべてのために、このチュートリアルもすぐに時代遅れのクラブに参加します。

非同期IOの中心にはコルーチンがあります。 コルーチンは、Pythonジェネレーター関数の特殊バージョンです。 ベースラインの定義から始めて、ここで進むにつれて構築します。コルーチンは、「+ return +」に達する前に実行を一時停止できる関数であり、しばらくの間間接的に別のコルーチンに制御を渡すことができます。

後で、従来のジェネレーターがコルーチンにどのように再利用されるかについて、さらに深く掘り下げます。 今のところ、コルーチンの動作を理解する最も簡単な方法は、コルーチンの作成を開始することです。

没入型のアプローチを取り、非同期IOコードを作成しましょう。 この短いプログラムは、非同期IOの「+ Hello World +」ですが、そのコア機能を説明するのに非常に役立ちます。

#!/usr/bin/env python3
# countasync.py

import asyncio

async def count():
    print("One")
    await asyncio.sleep(1)
    print("Two")

async def main():
    await asyncio.gather(count(), count(), count())

if __name__ == "__main__":
    import time
    s = time.perf_counter()
    asyncio.run(main())
    elapsed = time.perf_counter() - s
    print(f"{__file__} executed in {elapsed:0.2f} seconds.")

このファイルを実行するとき、 `+ def `と ` time.sleep()+`だけで関数を定義する場合とは異なるように見えることに注意してください:

$ python3 countasync.py
One
One
One
Two
Two
Two
countasync.py executed in 1.01 seconds.

この出力の順序は、非同期IOの中心です。 `+ count()`の各呼び出しと話すことは、単一のイベントループまたはコーディネーターです。 各タスクが「 await asyncio.sleep(1)+」に達すると、関数はイベントループに叫び、制御を戻し、「1秒間スリープします。 先に進み、その間に何か意味のあることをさせてください。」

これを同期バージョンと比較してください。

#!/usr/bin/env python3
# countsync.py

import time

def count():
    print("One")
    time.sleep(1)
    print("Two")

def main():
    for _ in range(3):
        count()

if __name__ == "__main__":
    s = time.perf_counter()
    main()
    elapsed = time.perf_counter() - s
    print(f"{__file__} executed in {elapsed:0.2f} seconds.")

実行すると、順序と実行時間にわずかではあるが重要な変更があります。

$ python3 countsync.py
One
Two
One
Two
One
Two
countsync.py executed in 3.01 seconds.

`+ time.sleep()`と ` asyncio.sleep()`を使用するのは当たり前のように思えるかもしれませんが、これらは待機時間を伴う時間のかかるプロセスの代役として使用されます。 (待つことができる最もありふれたものは、基本的に何もしない ` sleep()`呼び出しです。)つまり、 ` time.sleep()`は時間のかかるブロッキング関数呼び出しを表し、 ` asyncio.sleep()+ `は、ノンブロッキングコール(ただし、完了までに時間がかかるコール)の代わりに使用されます。

次のセクションで見るように、 `+ asyncio.sleep()`を含む何かを待つことの利点は、周囲の関数が、すぐに何かをより簡単に実行できる別の関数に制御を一時的に委譲できることです。 対照的に、 ` time.sleep()+`またはその他のブロッキング呼び出しは、スリープ時間の間トラック内のすべてを停止するため、非同期Pythonコードと互換性がありません。

非同期IOのルール

この時点で、「+ async 」、「 await 」、およびそれらが作成するコルーチン関数のより正式な定義が整いました。 このセクションはやや密集していますが、 ` async `/` await +`を取得することは有用ですので、必要な場合はこのセクションに戻ってください。

  • 構文 `+ async def `は、*ネイティブコルーチン*または*非同期ジェネレーター*を導入します。 「 async with 」および「 async for +」という表現も有効であり、後で見ることができます。

  • キーワード「+ await 」は、関数制御をイベントループに戻します。 (周囲のコルーチンの実行を一時停止します。)Pythonが ` g()`のスコープで ` await f()`式を検出した場合、これが ` await `がイベントループに指示する方法です。待っているものが何であれ、 ` f()`の結果が返されるまで、 ` g()+`の実行が返されます。 それまでの間、何か他のものを実行させてください。」

コードでは、2番目の箇条書きはおおよそ次のようになります。

async def g():
    # Pause here and come back to g() when f() is ready
    r = await f()
    return r

また、「+ async 」/「 await 」を使用できる場合と使用できない場合について、厳密な規則があります。 これらは、まだ構文を取り上げているか、すでに ` async `/` await +`の使用に触れている場合に便利です。

  • + async def +`で導入する関数はコルーチンです。 `+ await ++ return +、または `+ yield `を使用できますが、これらはすべてオプションです。 ` async def noop():pass +`の宣言は有効です:

  • `+ await `および/または ` return `を使用すると、コルーチン関数が作成されます。 コルーチン関数を呼び出すには、結果を取得するために「 await +」する必要があります。

  • `+ async def `ブロックで ` yield `を使用することはあまり一般的ではありません(Pythonで最近のみ有効です)。 これにより、https://www.python.org/dev/peps/pep-0525/[asynchronous generator]が作成され、 ` async for `で繰り返し処理されます。 当面は非同期ジェネレーターを忘れて、 ` await `や ` return +`を使用するコルーチン関数の構文を理解することに集中してください。

  • `+ async def `で定義されたものはすべて ` yield from `を使用できず、 ` SyntaxError +`が発生します。

  • 「+ def 」関数の外で「 yield 」を使用する「 SyntaxError 」のように、「 async def 」コルーチンの外で「 await 」を使用するのは「 SyntaxError 」です。 コルーチンの本体では、 ` await +`のみを使用できます。

上記のいくつかのルールを要約するための簡潔な例を次に示します。

async def f(x):
    y = await z(x)  # OK - `await` and `return` allowed in coroutines
    return y

async def g(x):
    yield x  # OK - this is an async generator

async def m(x):
    yield from gen(x)  # No - SyntaxError

def m(x):
    y = await z(x)  # Still no - SyntaxError (no `async def` here)
    return y

最後に、 + await f()+`を使用する場合、 `+ f()+`がhttps://docs.python.org/3/reference/datamodel.html#awaitable-objectsであるオブジェクトである必要があります[待ちに待った]。 まあ、それはあまり役に立ちませんよね? とりあえず、待機可能なオブジェクトは、(1)別のコルーチン、または(2)イテレータを返す `+ . await ()+ dunderメソッドを定義するオブジェクトであることを知ってください。 プログラムを作成している場合、大部分の目的のために、ケース1のみを心配する必要があります。

これにより、ポップアップが表示される可能性があるもう1つの技術的区別があります。関数をコルーチンとしてマークする古い方法は、通常の + def +`関数を `+ @ asyncio.coroutine +`で装飾することです。 結果は、*ジェネレーターベースのコルーチン*です。 Python 3.5で `+ async +/`+ await +`構文が導入されて以来、この構造は時代遅れになっています。

これらの2つのコルーチンは基本的に同等です(どちらも待機可能です)が、最初のコルーチンは*ジェネレーターベース*で、2つ目は*ネイティブコルーチンです:

import asyncio

@asyncio.coroutine
def py34_coro():
    """Generator-based coroutine, older syntax"""
    yield from stuff()

async def py35_coro():
    """Native coroutine, modern syntax"""
    await stuff()

自分でコードを記述している場合は、暗黙的ではなく明示的であるために、ネイティブコルーチンを好みます。 ジェネレーターベースのコルーチンは、Python 3.10ではhttps://docs.python.org/3/library/asyncio-task.html#generator-based-coroutines [削除]になります。

このチュートリアルの後半では、説明のためにジェネレータベースのコルーチンに触れます。 + async +/`+ await +`が導入された理由は、コルーチンを通常のジェネレーター関数と簡単に区別できるPythonのスタンドアロン機能にして、あいまいさを減らすためです。

https://www.python.org/dev/peps/pep-0492/#rationale-and-goals [意図的に時代遅れ]になったジェネレーターベースのコルーチンに「+ async + /」によって動揺しないでください。 + await + `。 これらには独自の小さなルールセットがあり(たとえば、ジェネレータベースのコルーチンでは `+ await +`は使用できません)、 `+ async +/`+ await +`構文に固執する場合はほとんど無関係です。

それ以上苦労することなく、さらにいくつかの複雑な例を取り上げましょう。

非同期IOが待機時間を削減する方法の1つの例を示します。範囲[0、10]のランダムな整数を生成し続けるコルーチン `+ makerandom()+`が与えられ、それらの1つがしきい値を超えるまで、複数のこのコルーチンの呼び出しは、お互いが連続して完了するのを待つ必要はありません。 上記の2つのスクリプトのパターンは、わずかな変更を加えてほぼ追跡できます。

#!/usr/bin/env python3
# rand.py

import asyncio
import random

# ANSI colors
c = (
    "\033[0m",   # End of color
    "\033[36m",  # Cyan
    "\033[91m",  # Red
    "\033[35m",  # Magenta
)

async def makerandom(idx: int, threshold: int = 6) -> int:
    print(c[idx + 1] + f"Initiated makerandom({idx}).")
    i = random.randint(0, 10)
    while i <= threshold:
        print(c[idx + 1] + f"makerandom({idx}) == {i} too low; retrying.")
        await asyncio.sleep(idx + 1)
        i = random.randint(0, 10)
    print(c[idx + 1] + f"---> Finished: makerandom({idx}) == {i}" + c[0])
    return i

async def main():
    res = await asyncio.gather(*(makerandom(i, 10 - i - 1) for i in range(3)))
    return res

if __name__ == "__main__":
    random.seed(444)
    r1, r2, r3 = asyncio.run(main())
    print()
    print(f"r1: {r1}, r2: {r2}, r3: {r3}")

色付けされた出力は、私ができる以上のことを言っており、このスクリプトがどのように実行されるかについての感覚を与えます:

このプログラムは、1つのメインコルーチンである `+ makerandom()`を使用し、3つの異なる入力で同時に実行します。 ほとんどのプログラムには、小さなモジュール式コルーチンと、小さなコルーチンのそれぞれを連結するのに役立つ1つのラッパー関数が含まれます。 次に、 ` main()+`を使用して、反復可能またはプール全体に中央コルーチンをマッピングすることにより、タスク(将来)を収集します。

このミニチュアの例では、プールは `+ range(3)`です。 後で紹介するより完全な例では、同時にリクエスト、解析、処理する必要があるURLのセットであり、 ` main()+`は各URLのルーチン全体をカプセル化します。

「ランダムな整数を作成する」(これは何よりもCPUにバインドされている)が `+ asyncio `の候補としては最大の選択ではないかもしれませんが、設計された例では ` asyncio.sleep()`が存在します待ち時間が不確実なIOにバインドされたプロセスを模倣する。 たとえば、 ` asyncio.sleep()+`呼び出しは、メッセージアプリケーション内の2つのクライアント間でそれほどランダムではない整数を送受信することを表します。

非同期IOデザインパターン

非同期IOには、独自の可能なスクリプトデザインのセットが付属しています。これについては、このセクションで紹介します。

コルーチンの連鎖

コルーチンの重要な特徴は、それらを一緒に連鎖できることです。 (コルーチンオブジェクトは待機可能であるため、別のコルーチンはそれを「+ await +」することができます。)これにより、プログラムをより小さく管理しやすいリサイクル可能なコルーチンに分割できます。

#!/usr/bin/env python3
# chained.py

import asyncio
import random
import time

async def part1(n: int) -> str:
    i = random.randint(0, 10)
    print(f"part1({n}) sleeping for {i} seconds.")
    await asyncio.sleep(i)
    result = f"result{n}-1"
    print(f"Returning part1({n}) == {result}.")
    return result

async def part2(n: int, arg: str) -> str:
    i = random.randint(0, 10)
    print(f"part2{n, arg} sleeping for {i} seconds.")
    await asyncio.sleep(i)
    result = f"result{n}-2 derived from {arg}"
    print(f"Returning part2{n, arg} == {result}.")
    return result

async def chain(n: int) -> None:
    start = time.perf_counter()
    p1 = await part1(n)
    p2 = await part2(n, p1)
    end = time.perf_counter() - start
    print(f"-->Chained result{n} => {p2} (took {end:0.2f} seconds).")

async def main(*args):
    await asyncio.gather(*(chain(n) for n in args))

if __name__ == "__main__":
    import sys
    random.seed(444)
    args = [1, 2, 3] if len(sys.argv) == 1 else map(int, sys.argv[1:])
    start = time.perf_counter()
    asyncio.run(main(*args))
    end = time.perf_counter() - start
    print(f"Program finished in {end:0.2f} seconds.")

出力に注意してください。ここでは、 `+ part1()`が可変時間スリープし、 ` part2()+`が使用可能になると結果の処理を開始します。

$ python3 chained.py 9 6 3
part1(9) sleeping for 4 seconds.
part1(6) sleeping for 4 seconds.
part1(3) sleeping for 0 seconds.
Returning part1(3) == result3-1.
part2(3, 'result3-1') sleeping for 4 seconds.
Returning part1(9) == result9-1.
part2(9, 'result9-1') sleeping for 7 seconds.
Returning part1(6) == result6-1.
part2(6, 'result6-1') sleeping for 4 seconds.
Returning part2(3, 'result3-1') == result3-2 derived from result3-1.
-->Chained result3 => result3-2 derived from result3-1 (took 4.00 seconds).
Returning part2(6, 'result6-1') == result6-2 derived from result6-1.
-->Chained result6 => result6-2 derived from result6-1 (took 8.01 seconds).
Returning part2(9, 'result9-1') == result9-2 derived from result9-1.
-->Chained result9 => result9-2 derived from result9-1 (took 11.01 seconds).
Program finished in 11.01 seconds.

このセットアップでは、 `+ main()+`のランタイムは、一緒に収集してスケジュールするタスクの最大ランタイムに等しくなります。

キューを使用する

+ asyncio +`パッケージは、https://docs.python.org/のクラスに類似するように設計されたhttps://docs.python.org/3/library/asyncio-queue.html [キュークラス]を提供します3/library/queue.html#module-queue [+ queue `]モジュール。 これまでの例では、実際にはキュー構造は必要ありませんでした。 ` chained.py +`では、各タスク(将来)は、相互に明示的に待機し、チェーンごとに1つの入力を渡すコルーチンのセットで構成されます。

非同期IOでも機能する代替構造があります。相互に関連付けられていない多数のプロデューサーが、アイテムをキューに追加します。 各プロデューサーは、時間をずらしてランダムに予告なしに複数のアイテムをキューに追加できます。 消費者のグループは、他の信号を待たずに、欲張りにキューからアイテムを引き出します。

この設計では、個々の消費者から生産者への連鎖はありません。 消費者は、生産者の数や、キューに追加されるアイテムの累積数さえも事前に知りません。

個々の生産者または消費者は、それぞれキューにアイテムを入れたり抽出したりするのにさまざまな時間がかかります。 キューは、生産者と消費者が直接対話せずに生産者と消費者と通信できるスループットとして機能します。

注意:スレッドプログラムでは、 `+ queue.Queue()+`のスレッドセーフのためにキューがよく使用されますが、非同期IOに関しては、スレッドセーフについて心配する必要はありません。 (例外は2つを組み合わせる場合ですが、このチュートリアルでは行いません。)

キューの使用例の1つは(この場合のように)、キューがプロデューサーとコンシューマーのトランスミッタとして機能することです。

このプログラムの同期バージョンは非常に悲惨に見えます。ブロッキングプロデューサーのグループは、一度に1つのプロデューサーをキューにアイテムを順次追加します。 すべてのプロデューサーが完了して初めて、アイテムごとに1人のコンシューマーが一度に処理し、キューを処理できます。 この設計には膨大な遅延があります。 アイテムはすぐに取り出されて処理されるのではなく、キューに放置される場合があります。

非同期バージョンの `+ asyncq.py `は以下にあります。 このワークフローの難しい部分は、生産が行われたことを消費者に知らせる必要があるということです。 そうしないと、キューが完全に処理されるため、 ` await q.get()+`が無期限にハングアップしますが、消費者は生産が完了したことを知ることができません。

(StackOverflow userからの `+ main()`をまっすぐにするのを助けてくれてありがとう:キーは ` await q.join()にある+ `、キュー内のすべてのアイテムが受信および処理されるまでブロックし、コンシューマータスクをキャンセルします。そうしないと、ハングアップし、追加のキューアイテムが表示されるまで無限に待機します。

これが完全なスクリプトです。

#!/usr/bin/env python3
# asyncq.py

import asyncio
import itertools as it
import os
import random
import time

async def makeitem(size: int = 5) -> str:
    return os.urandom(size).hex()

async def randsleep(a: int = 1, b: int = 5, caller=None) -> None:
    i = random.randint(0, 10)
    if caller:
        print(f"{caller} sleeping for {i} seconds.")
    await asyncio.sleep(i)

async def produce(name: int, q: asyncio.Queue) -> None:
    n = random.randint(0, 10)
    for _ in it.repeat(None, n):  # Synchronous loop for each single producer
        await randsleep(caller=f"Producer {name}")
        i = await makeitem()
        t = time.perf_counter()
        await q.put((i, t))
        print(f"Producer {name} added <{i}> to queue.")

async def consume(name: int, q: asyncio.Queue) -> None:
    while True:
        await randsleep(caller=f"Consumer {name}")
        i, t = await q.get()
        now = time.perf_counter()
        print(f"Consumer {name} got element <{i}>"
              f" in {now-t:0.5f} seconds.")
        q.task_done()

async def main(nprod: int, ncon: int):
    q = asyncio.Queue()
    producers = [asyncio.create_task(produce(n, q)) for n in range(nprod)]
    consumers = [asyncio.create_task(consume(n, q)) for n in range(ncon)]
    await asyncio.gather(*producers)
    await q.join()  # Implicitly awaits consumers, too
    for c in consumers:
        c.cancel()

if __name__ == "__main__":
    import argparse
    random.seed(444)
    parser = argparse.ArgumentParser()
    parser.add_argument("-p", "--nprod", type=int, default=5)
    parser.add_argument("-c", "--ncon", type=int, default=10)
    ns = parser.parse_args()
    start = time.perf_counter()
    asyncio.run(main(**ns.__dict__))
    elapsed = time.perf_counter() - start
    print(f"Program completed in {elapsed:0.5f} seconds.")

最初のいくつかのコルーチンは、ランダムな文字列、小数秒のパフォーマンスカウンター、およびランダムな整数を返すヘルパー関数です。 プロデューサーは1〜5個のアイテムをキューに入れます。 各項目は「(i、t)」のタプルです。「+ i 」はランダムな文字列で、「 t +」はプロデューサーがタプルをキューに入れようとする時間です。

消費者がアイテムを引き出すとき、アイテムが入れられたタイムスタンプを使用して、アイテムがキューに座っている経過時間を単に計算します。

`+ asyncio.sleep()+`は、通常のブロッキング関数である場合、時間を浪費し、他のすべての実行をブロックする他のより複雑なコルーチンを模倣するために使用されることに留意してください。

これは、2つのプロデューサーと5つのコンシューマーによるテスト実行です。

$ python3 asyncq.py -p 2 -c 5
Producer 0 sleeping for 3 seconds.
Producer 1 sleeping for 3 seconds.
Consumer 0 sleeping for 4 seconds.
Consumer 1 sleeping for 3 seconds.
Consumer 2 sleeping for 3 seconds.
Consumer 3 sleeping for 5 seconds.
Consumer 4 sleeping for 4 seconds.
Producer 0 added <377b1e8f82> to queue.
Producer 0 sleeping for 5 seconds.
Producer 1 added <413b8802f8> to queue.
Consumer 1 got element <377b1e8f82> in 0.00013 seconds.
Consumer 1 sleeping for 3 seconds.
Consumer 2 got element <413b8802f8> in 0.00009 seconds.
Consumer 2 sleeping for 4 seconds.
Producer 0 added <06c055b3ab> to queue.
Producer 0 sleeping for 1 seconds.
Consumer 0 got element <06c055b3ab> in 0.00021 seconds.
Consumer 0 sleeping for 4 seconds.
Producer 0 added <17a8613276> to queue.
Consumer 4 got element <17a8613276> in 0.00022 seconds.
Consumer 4 sleeping for 5 seconds.
Program completed in 9.00954 seconds.

この場合、アイテムはほんの一瞬で処理されます。 遅延の原因は2つあります。

  • 標準で、ほとんど回避できないオーバーヘッド

  • アイテムがキューに表示されたときにすべての消費者が眠っている状況

幸いなことに、2番目の理由に関しては、数百または数千の消費者に対応することは完全に正常です。 `+ python3 asyncq.py -p 5 -c 100 +`で問題はないはずです。 ここでのポイントは、理論的には、異なるシステムの異なるユーザーがプロデューサーとコンシューマーの管理を制御し、キューが中央スループットとして機能することです。

これまで、あなたは火の中に投げ込まれ、 `+ async `と ` await `で定義されたコルーチンを呼び出す ` asyncio +`の関連する3つの例を見てきました。 Pythonで現代のコルーチンがどのようになったのかを完全にフォローしていない場合、または単に深く掘り下げたい場合は、次のセクションから正方形から始めます。

ジェネレーターの非同期IOのルーツ

前に、より明確なネイティブコルーチンによって時代遅れにされた古いスタイルのジェネレータベースのコルーチンの例を見てきました。 この例は、少し調整して再表示する価値があります。

import asyncio

@asyncio.coroutine
def py34_coro():
    """Generator-based coroutine"""
    # No need to build these yourself, but be aware of what they are
    s = yield from stuff()
    return s

async def py35_coro():
    """Native coroutine, modern syntax"""
    s = await stuff()
    return s

async def stuff():
    return 0x10, 0x20, 0x30

実験として、 + py34_coro()+`または `+ py35_coro()+`を単独で、 `+ await +`なしで、または `+ asyncio.run()+`などの呼び出しなしで呼び出すとどうなりますか+ asyncio +`「磁器」機能? コルーチンを単独で呼び出すと、コルーチンオブジェクトが返されます。

>>>

>>> py35_coro()
<coroutine object py35_coro at 0x10126dcc8>

これは表面上はあまり面白くないです。 コルーチンを単独で呼び出した結果は、待望の*コルーチンオブジェクト*です。

クイズの時間:Pythonの他の機能は次のようになりますか? (Pythonのどの機能が、それ自体で呼び出されたときに実際に「多くのことをしない」のですか?)

コルーチンは内部の拡張ジェネレータであるため、この質問に対する答えとして*ジェネレータ*を考えていることを願っています。 この点で動作は似ています:

>>>

>>> def gen():
...     yield 0x10, 0x20, 0x30
...
>>> g = gen()
>>> g  # Nothing much happens - need to iterate with `.__next__()`
<generator object gen at 0x1012705e8>
>>> next(g)
(16, 32, 48)

ジェネレーター関数は、そうなると、非同期IOの基盤になります(古いルーチンの `+ @ asyncio.coroutine `ラッパーではなく、 ` async def `でコルーチンを宣言するかどうかに関係なく)。 技術的には、「 await 」は「 yield 」よりも「 yield from 」により類似しています。 (ただし、 `( yield from x()`はx()の ` for i:yield i +`を置き換えるための単なる構文上のシュガーであることに注意してください。)

非同期IOに関連するジェネレータの重要な機能の1つは、ジェネレータを自由に効果的に停止および再起動できることです。 たとえば、ジェネレータオブジェクトの反復処理を「+ break 」して、後で残りの値の反復処理を再開できます。 ジェネレーター関数が ` yield +`に達すると、その値を生成しますが、その後の値を生成するように指示されるまでアイドル状態になります。

これは例を通して具体化できます:

>>>

>>> from itertools import cycle
>>> def endless():
...     """Yields 9, 8, 7, 6, 9, 8, 7, 6, ... forever"""
...     yield from cycle((9, 8, 7, 6))

>>> e = endless()
>>> total = 0
>>> for i in e:
...     if total < 30:
...         print(i, end=" ")
...         total += i
...     else:
...         print()
...         # Pause execution. We can resume later.
...         break
9 8 7 6 9 8 7 6 9 8 7 6 9 8

>>> # Resume
>>> next(e), next(e), next(e)
(6, 9, 8)

`+ await `キーワードも同様に動作し、コルーチンが自身を中断し、他のコルーチンを機能させるブレークポイントをマークします。 この場合、「一時停止」とは、一時的に制御権を放棄したが、完全に終了または終了していないコルーチンを意味します。 「 yield 」、および拡張機能「 yield from 」と「 await +」により、ジェネレーターの実行のブレークポイントをマークすることに注意してください。

これは、関数とジェネレーターの根本的な違いです。 関数はオールオアナッシングです。 一度開始すると、 `+ return `に達するまで停止せず、その値を呼び出し元(呼び出し元の関数)にプッシュします。 一方、ジェネレーターは、「 yield 」に達するたびに一時停止し、それ以上進みません。 この値を呼び出しスタックにプッシュできるだけでなく、 ` next()+`を呼び出して再開するときにローカル変数を保持できます。

ジェネレータの2番目のあまり知られていない機能も重要です。 + .send()+`メソッドを使用して、ジェネレーターに値を送信することもできます。 これにより、ジェネレーター(およびコルーチン)はブロックせずに互いに呼び出し( `+ await +)できます。 これは主に舞台裏でコルーチンを実装するために重要であるため、この機能の詳細は説明しませんが、実際に直接使用する必要はありません。

さらに詳しく知りたい場合は、https://www.python.org/dev/peps/pep-0342/[PEP 342]から始めてください。コルーチンが正式に紹介されています。 Brett Cannonのhttps://snarky.ca/how-the-heck-does-async-await-work-in-python-3-5/[PythonでHeckがAsync-Awaitを機能させる方法]も良い読み物です。 https://pymotw.com/3/asyncio/coroutines.html [`+ asyncio +`のPYMOTWの記事]と同様です。 最後に、David Beazleyのhttp://www.dabeaz.com/coroutines/[コルーチンと並行性に関する好奇心の強いコース]があります。これは、コルーチンが実行されるメカニズムの詳細です。

上記のすべての記事をいくつかの文章にまとめてみましょう。これらのコルーチンが実際に実行される特に型破りなメカニズムがあります。 それらの結果は、 `+ .send()+`メソッドが呼び出されたときにスローされる例外オブジェクトの属性です。 これらのすべてにはさらに奇妙な詳細がありますが、おそらく実際に言語のこの部分を使用するのに役立つことはないので、今から先に進みましょう。

物事をまとめるために、ジェネレーターとしてのコルーチンのトピックに関する重要なポイントを以下に示します。

  • コルーチンはhttps://www.python.org/dev/peps/pep-0492/#differences-from-generators[repurposed generators]であり、ジェネレーターメソッドの特性を利用しています。

  • 古いジェネレータベースのコルーチンは、コルーチンの結果を待つために「+ yield from 」を使用します。 ネイティブコルーチンの最新のPython構文は、コルーチンの結果を待機する手段として、単に「 yield from 」を「 await 」に置き換えます。 「 await 」は「 yield from +」に類似しており、多くの場合、そのように考えるのに役立ちます。

  • `+ await +`の使用は、ブレークポイントをマークするシグナルです。 これにより、コルーチンは一時的に実行を一時停止し、プログラムが後でそれに戻ることを許可します。

その他の機能: `+ async for +`およびAsync Generators + Comprehensions

Pythonは、単純な + async +/`+ await `に加えて、 ` async for +`が*非同期イテレータ*を反復処理できるようにします。 非同期イテレーターの目的は、反復されるときに各段階で非同期コードを呼び出すことができるようにすることです。

この概念の自然な拡張は、*非同期ジェネレータ*です。 ネイティブコルーチンで + await ++ return +、または `+ yield `を使用できることを思い出してください。 Python 3.6(PEP 525経由)でコルーチン内で「 yield 」を使用できるようになり、同じコルーチン関数本体で「 await 」と「 yield +」を使用できるようにする非同期ジェネレーターが導入されました。

>>>

>>> async def mygen(u: int = 10):
...     """Yield powers of 2."""
...     i = 0
...     while i < u:
...         yield 2 ** i
...         i += 1
...         await asyncio.sleep(0.1)

最後になりましたが、Pythonは、 `+ async for +`を使用して asynchronous comprehension を有効にします。 同期のいとこと同様、これは大部分が構文糖です:

>>>

>>> async def main():
...     # This does *not* introduce concurrent execution
...     # It is meant to show syntax only
...     g = [i async for i in mygen()]
...     f = [j async for j in mygen() if not (j//3 % 5)]
...     return g, f
...
>>> g, f = asyncio.run(main())
>>> g
[1, 2, 4, 8, 16, 32, 64, 128, 256, 512]
>>> f
[1, 2, 16, 32, 256, 512]

これは重要な違いです:非同期ジェネレーターも理解も、反復を並行させません。 それらが行うことは、同期の対応物のルックアンドフィールを提供することですが、問題のループが他のコルーチンを実行するためにイベントループに制御を放棄する機能を備えています。

つまり、非同期イテレーターと非同期ジェネレーターは、シーケンスまたはイテレーターを介して一部の関数を同時にマップするようには設計されていません。 単にコルーチンを囲むことで、他のタスクが順番を回せるように設計されています。 `+ async for `ステートメントと ` async with `ステートメントは、プレーンな ` for `または ` with `を使用すると、コルーチン内の ` await +`の性質を「破壊」する程度にのみ必要です。 非同期性と同時実行性のこの区別は、把握すべき重要なものです。

イベントループと + asyncio.run()+

イベントループは、コルーチンを監視し、アイドル状態に関するフィードバックを取得し、その間に実行できるものを探す「+ while True +」ループのようなものと考えることができます。 コルーチンが待機しているものが利用可能になったときに、アイドルコルーチンを起動することができます。

これまで、イベントループの管理全体は、1つの関数呼び出しによって暗黙的に処理されてきました。

asyncio.run(main())  # Python 3.7+

Python 3.7で導入されたhttps://github.com/python/cpython/blob/d4c76d960b8b286b75c933780416ace9cda682fd/Lib/asyncio/runners.py#L8 [+ asyncio.run()+]は、イベントループを取得し、完了としてマークされるまでタスクを実行し、イベントループを閉じます。

`+ get_event_loop()`を使用して、 ` asyncio +`イベントループを管理するより長い方法があります。 典型的なパターンは次のようになります。

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

古い例では、おそらく `+ loop.get_event_loop()`が浮かんでいるでしょうが、イベントループ管理の制御を微調整する必要が特にない限り、 ` asyncio.run()+`で十分です。ほとんどのプログラム。

Pythonプログラム内でイベントループとやり取りする必要がある場合、 `+ loop `は、 ` loop.is_running()`および ` loop.is_closed()+によるイントロスペクションをサポートする古風なPythonオブジェクトです。 `。 https://docs.python.org/3/library/asyncio-eventloop.html#asyncio-example-lowlevel-helloworld [コールバックのスケジューリング]のように、さらに微調整した制御を取得する必要がある場合は、それを操作できます。ループを引数として渡すことにより。

さらに重要なのは、イベントループの仕組みについて少し表面的に理解することです。 イベントループについて強調する価値があるいくつかのポイントを次に示します。

*#1:*コルーチンは、イベントループに結び付けられるまで、単独ではあまり機能しません。

前に発電機の説明でこの点を見ましたが、もう一度説明する価値があります。 他の人を待つメインコルーチンがある場合、それを単独で呼び出すだけではほとんど効果がありません。

>>>

>>> import asyncio

>>> async def main():
...     print("Hello ...")
...     await asyncio.sleep(1)
...     print("World!")

>>> routine = main()
>>> routine
<coroutine object main at 0x1027a6150>

イベントループでの実行のために `+ main()`コルーチン(将来のオブジェクト)をスケジュールすることにより、実際に強制的に実行するために ` asyncio.run()+`を使用することを忘れないでください:

>>>

>>> asyncio.run(routine)
Hello ...
World!

(他のコルーチンは `+ await `で実行できます。 ` asyncio.run()`で ` main()`だけをラップするのが一般的で、 ` await +`でチェーンされたコルーチンがそこから呼び出されます。)

*#2:*デフォルトでは、非同期IOイベントループは単一のスレッドと単一のCPUコアで実行されます。 通常、1つのCPUコアで1つのシングルスレッドイベントループを実行するだけで十分です。 複数のコアでイベントループを実行することもできます。 このhttps://youtu.be/0kXaLh8Fz3k?t=10m30s[John Reeseによるトーク]をチェックして、ラップトップが自発的に燃焼する可能性があることを警告してください。

*#3. *イベントループはプラグ可能です。 つまり、本当に必要な場合は、独自のイベントループ実装を記述し、タスクをまったく同じように実行させることができます。 これは、Cythonのイベントループの実装であるhttps://github.com/MagicStack/uvloop [+ uvloop +]パッケージで見事に実証されています。

それが「プラグ可能なイベントループ」という用語の意味です。コルーチン自体の構造とは関係なく、イベントループの実装を使用できます。 + asyncio +`パッケージ自体には、https://docs.python.org/3/library/asyncio-eventloop.html#event-loop-implementations [2つの異なるイベントループ実装]が付属しています。デフォルトはhttpsに基づいています://docs.python.org/3/library/selectors.html#module-selectors [+ selectors +`]モジュール。 (2番目の実装はWindows専用に構築されています。)

完全なプログラム:非同期リクエスト

ここまでで、これで楽しさと痛みのない部分の時間です。 このセクションでは、非常に高速な非同期HTTPクライアント/サーバーフレームワークである「+ aiohttp 」を使用して、WebスクレイピングURLコレクター「 areq.py +」を構築します。 (クライアント部分だけが必要です。)このようなツールを使用して、サイトのクラスター間の接続をマッピングし、リンクがhttps://en.wikipedia.org/wiki/Directed_graph [有向グラフ]を形成するようにすることができます。

:Pythonの `+ requests `パッケージが非同期IOと互換性がないのはなぜかと思うかもしれません。 ` requests `は ` urllib3 `の上に構築され、Pythonの ` http `および ` socket +`モジュールを使用します。

デフォルトでは、ソケット操作はブロックされています。 これは、 + .get()+`が待機可能でないため、Pythonは `+ await requests.get(url)+`を好まないことを意味します。 対照的に、 `+ aiohttp +`のほとんどすべては、https://github.com/aio-libs/aiohttp/blob/508adbb656da2e9ae660da5e98e1e5fa6669a3f4/aiohttp/client.py#L225 [+ session.request()などの待望のコルーチンです。 + ]およびhttps://github.com/aio-libs/aiohttp/blob/da75122f6089a250128d2736f2bd88d10e97ca17/aiohttp/client_reqrep.py#L913 [ + response.text()+ `]。 それ以外の場合は素晴らしいパッケージですが、非同期コードで `+ requests +`を使用することにより、自分自身を傷つけています。

高レベルのプログラム構造は次のようになります。

  1. ローカルファイル `+ urls.txt +`から一連のURLを読み取ります。

  2. URLのGETリクエストを送信し、結果のコンテンツをデコードします。 これが失敗した場合、URLのためにそこで停止します。

  3. 応答のHTMLの `+ href +`タグ内でURLを検索します。

  4. 結果を `+ foundurls.txt +`に書き込みます。

  5. 上記のすべてを可能な限り非同期で同時に実行します。 (リクエストには `+ aiohttp `を、ファイルの追加には ` aiofiles +`を使用します。 これらは、非同期IOモデルに適したIOの2つの主要な例です。

以下は `+ urls.txt +`の内容です。 巨大ではなく、ほとんどがトラフィックの多いサイトが含まれています。

$ cat urls.txt
https://regex101.com/
https://docs.python.org/3/this-url-will-404.html
https://www.nytimes.com/guides/
https://www.mediamatters.org/
https://1.1.1.1/
https://www.politico.com/tipsheets/morning-money
https://www.bloomberg.com/markets/economics
https://www.ietf.org/rfc/rfc2616.txt

リストの2番目のURLは404応答を返す必要があります。これは適切に処理する必要があります。 このプログラムの拡張バージョンを実行している場合は、おそらくサーバーの切断や無限のリダイレクトなど、これよりもはるかに難しい問題に対処する必要があります。

リクエスト自体は、セッションの内部接続プールの再利用を活用するために、単一のセッションを使用して作成する必要があります。

完全なプログラムを見てみましょう。 以下の手順を順を追って説明します。

#!/usr/bin/env python3
# areq.py

"""Asynchronously get links embedded in multiple pages' HMTL."""

import asyncio
import logging
import re
import sys
from typing import IO
import urllib.error
import urllib.parse

import aiofiles
import aiohttp
from aiohttp import ClientSession

logging.basicConfig(
    format="%(asctime)s %(levelname)s:%(name)s: %(message)s",
    level=logging.DEBUG,
    datefmt="%H:%M:%S",
    stream=sys.stderr,
)
logger = logging.getLogger("areq")
logging.getLogger("chardet.charsetprober").disabled = True

HREF_RE = re.compile(r'href="(.*?)"')

async def fetch_html(url: str, session: ClientSession, **kwargs) -> str:
    """GET request wrapper to fetch page HTML.

    kwargs are passed to `session.request()`.
    """

    resp = await session.request(method="GET", url=url, **kwargs)
    resp.raise_for_status()
    logger.info("Got response [%s] for URL: %s", resp.status, url)
    html = await resp.text()
    return html

async def parse(url: str, session: ClientSession, **kwargs) -> set:
    """Find HREFs in the HTML of `url`."""
    found = set()
    try:
        html = await fetch_html(url=url, session=session, **kwargs)
    except (
        aiohttp.ClientError,
        aiohttp.http_exceptions.HttpProcessingError,
    ) as e:
        logger.error(
            "aiohttp exception for %s [%s]: %s",
            url,
            getattr(e, "status", None),
            getattr(e, "message", None),
        )
        return found
    except Exception as e:
        logger.exception(
            "Non-aiohttp exception occured:  %s", getattr(e, "__dict__", {})
        )
        return found
    else:
        for link in HREF_RE.findall(html):
            try:
                abslink = urllib.parse.urljoin(url, link)
            except (urllib.error.URLError, ValueError):
                logger.exception("Error parsing URL: %s", link)
                pass
            else:
                found.add(abslink)
        logger.info("Found %d links for %s", len(found), url)
        return found

async def write_one(file: IO, url: str, **kwargs) -> None:
    """Write the found HREFs from `url` to `file`."""
    res = await parse(url=url, **kwargs)
    if not res:
        return None
    async with aiofiles.open(file, "a") as f:
        for p in res:
            await f.write(f"{url}\t{p}\n")
        logger.info("Wrote results for source URL: %s", url)

async def bulk_crawl_and_write(file: IO, urls: set, **kwargs) -> None:
    """Crawl & write concurrently to `file` for multiple `urls`."""
    async with ClientSession() as session:
        tasks = []
        for url in urls:
            tasks.append(
                write_one(file=file, url=url, session=session, **kwargs)
            )
        await asyncio.gather(*tasks)

if __name__ == "__main__":
    import pathlib
    import sys

    assert sys.version_info >= (3, 7), "Script requires Python 3.7+."
    here = pathlib.Path(__file__).parent

    with open(here.joinpath("urls.txt")) as infile:
        urls = set(map(str.strip, infile))

    outpath = here.joinpath("foundurls.txt")
    with open(outpath, "w") as outfile:
        outfile.write("source_url\tparsed_url\n")

    asyncio.run(bulk_crawl_and_write(file=outpath, urls=urls))

このスクリプトは、最初のおもちゃプログラムよりも長いので、分解してみましょう。

定数 `+ HREF_RE `は、最終的に検索対象となるHTML内の ` href +`タグを抽出するための正規表現です。

>>>

>>> HREF_RE.search('Go to <a href="https://realpython.com/">Real Python</a>')
<re.Match object; span=(15, 45), match='href="https://realpython.com/"'>

コルーチンの `+ fetch_html()+`は、リクエストを作成し、結果のページHTMLをデコードするGETリクエストのラッパーです。 200以外のステータスの場合、リクエストを作成し、レスポンスを待機し、すぐに発生します。

resp = await session.request(method="GET", url=url, **kwargs)
resp.raise_for_status()

ステータスに問題がない場合、 + fetch_html()+`はページHTML( `+ str +)を返します。 特に、この関数では例外処理は行われません。 ロジックは、その例外を呼び出し元に伝播し、そこで処理できるようにすることです。

html = await resp.text()

「+ await 」、「 session.request()」、および「 resp.text()」は待機可能なコルーチンであるためです。 それ以外の場合、要求/応答サイクルはアプリケーションの時間のかかる部分になりますが、非同期IOを使用すると、 ` fetch_html()+`により、URLの解析や書き込みなど、他の容易に利用可能なジョブでイベントループが機能します既に取得されています。

コルーチンのチェーンの次は `+ parse()`で、これは ` fetch_html()`で特定のURLを待機し、そのページのHTMLからすべての ` href +`タグを抽出して、それぞれが有効であり、絶対パスとしてフォーマットします。

確かに、 `+ parse()+`の2番目の部分はブロックされていますが、正規表現の迅速な一致と、検出されたリンクが絶対パスになっていることの確認で構成されています。

この特定のケースでは、この同期コードは迅速かつ目立たないはずです。 ただし、特定のコルーチン内の任意の行は、その行が「+ yield 」、「 await 」、または「 return 」を使用しない限り、他のコルーチンをブロックすることを覚えておいてください。 解析がより集中的なプロセスである場合は、https://docs.python.org/3/library/asyncio-eventloop.html#executing-code-in-thread-を使用して、この部分を独自のプロセスで実行することを検討できます。 or-process-pools [` loop.run_in_executor()+`]。

次に、コルーチンの `+ write()`はファイルオブジェクトと単一のURLを受け取り、 ` parse()`を待って解析されたURLの ` set `を返し、それぞれを非同期にファイルに書き込みます非同期ファイルIOのパッケージである ` aiofiles +`を使用したソースURL。

最後に、 `+ bulk_crawl_and_write()`は、スクリプトのコルーチンのチェーンへの主要なエントリポイントとして機能します。 単一のセッションを使用し、最終的に ` urls.txt +`から読み取られるURLごとにタスクが作成されます。

言及するに値するいくつかの追加ポイントを次に示します。

  • デフォルトの `+ ClientSession `にはhttps://aiohttp.readthedocs.io/en/stable/client_reference.html#connectors[adapter]があり、最大100のオープン接続があります。 これを変更するには、 ` asyncio.connector.TCPConnector `のインスタンスを ` ClientSession +`に渡します。 ホストごとに制限を指定することもできます。

  • セッション全体と個々のリクエストの両方に対して、最大https://aiohttp.readthedocs.io/en/stable/client_quickstart.html#timeouts[timeouts]を指定できます。

  • このスクリプトは、「+ async with 」も使用します。これはhttps://www.python.org/dev/peps/pep-0492/#asynchronous-context-managers-and-async-with[asynchronous context manager]で動作します。 同期から非同期のコンテキストマネージャーへの移行は非常に簡単なので、この概念にセクション全体を費やしたことはありません。 後者は、「 . exit ()」と「 . enter ()」ではなく、「 . aenter ()」と「 . aexit ()」を定義する必要があります。 ご想像のとおり、「 async with 」は、「 async def +」で宣言されたコルーチン関数内でのみ使用できます。

GitHubのこのチュートリアルのhttps://github.com/realpython/materials/tree/master/asyncio-walkthrough[companion files]には、コメントとdocstringsも添付されています。

`+ areq.py +`が1秒以内に9つのURLの結果を取得、解析、保存するための、すべての栄光の中での実行です。

$ python3 areq.py
21:33:22 DEBUG:asyncio: Using selector: KqueueSelector
21:33:22 INFO:areq: Got response [200] for URL: https://www.mediamatters.org/
21:33:22 INFO:areq: Found 115 links for https://www.mediamatters.org/
21:33:22 INFO:areq: Got response [200] for URL: https://www.nytimes.com/guides/
21:33:22 INFO:areq: Got response [200] for URL: https://www.politico.com/tipsheets/morning-money
21:33:22 INFO:areq: Got response [200] for URL: https://www.ietf.org/rfc/rfc2616.txt
21:33:22 ERROR:areq: aiohttp exception for https://docs.python.org/3/this-url-will-404.html [404]: Not Found
21:33:22 INFO:areq: Found 120 links for https://www.nytimes.com/guides/
21:33:22 INFO:areq: Found 143 links for https://www.politico.com/tipsheets/morning-money
21:33:22 INFO:areq: Wrote results for source URL: https://www.mediamatters.org/
21:33:22 INFO:areq: Found 0 links for https://www.ietf.org/rfc/rfc2616.txt
21:33:22 INFO:areq: Got response [200] for URL: https://1.1.1.1/
21:33:22 INFO:areq: Wrote results for source URL: https://www.nytimes.com/guides/
21:33:22 INFO:areq: Wrote results for source URL: https://www.politico.com/tipsheets/morning-money
21:33:22 INFO:areq: Got response [200] for URL: https://www.bloomberg.com/markets/economics
21:33:22 INFO:areq: Found 3 links for https://www.bloomberg.com/markets/economics
21:33:22 INFO:areq: Wrote results for source URL: https://www.bloomberg.com/markets/economics
21:33:23 INFO:areq: Found 36 links for https://1.1.1.1/
21:33:23 INFO:areq: Got response [200] for URL: https://regex101.com/
21:33:23 INFO:areq: Found 23 links for https://regex101.com/
21:33:23 INFO:areq: Wrote results for source URL: https://regex101.com/
21:33:23 INFO:areq: Wrote results for source URL: https://1.1.1.1/

それは粗末ではありません! 健全性チェックとして、出力の行数を確認できます。 私の場合、626ですが、これは変動する可能性があることに注意してください。

$ wc -l foundurls.txt
     626 foundurls.txt

$ head -n 3 foundurls.txt
source_url  parsed_url
https://www.bloomberg.com/markets/economics https://www.bloomberg.com/feedback
https://www.bloomberg.com/markets/economics https://www.bloomberg.com/notices/tos

次のステップ:アンティを上げたい場合は、このウェブクローラーを再帰的にします。 https://github.com/aio-libs/aioredis [+ aio-redis +]を使用して、ツリー内でクロールされたURLを追跡し、2回のリクエストを回避し、リンクをPythonの `+ networkx +に接続できます。 `ライブラリ。

素敵であることを忘れないでください。 小規模で疑いのないWebサイトに1000件の同時要求を送信するのは、悪い、悪い、悪いです。 `+ asyncio `のhttps://stackoverflow.com/q/40836800/7954504[sempahore]オブジェクトを使用する、またはパターンhttpsを使用するなど、1つのバッチで作成する同時リクエストの数を制限する方法があります。//www.artificialworlds.net/blog/2017/05/31/python-3-large-numbers-of-tasks-with-limited-concurrency/[このように]。 この警告に注意を払わないと、大量の ` TimeoutError +`例外が発生し、独自のプログラムに損害を与える可能性があります。

コンテキスト内の非同期IO

正常な量のコードを見てきたので、少し戻って、非同期IOが理想的なオプションである場合と、その結論に到達するために比較を行う方法、または別の並行モデルを選択する方法を考えてみましょう。

非同期IOが正しい選択である理由と理由

このチュートリアルは、非同期IO、スレッド化、マルチプロセッシングに関する拡張された論文には適していません。 ただし、非同期IOがおそらく3つのうちの最良の候補である時期を把握しておくと便利です。

非同期IOとマルチプロセッシングをめぐる戦いは、実際にはまったく戦いではありません。 実際、これらはhttps://youtu.be/0kXaLh8Fz3k?t=10m30s [コンサートで使用]になります。 複数のかなり均一なCPUバウンドタスクがある場合(素晴らしい例は、 `+ scikit-learn `などのライブラリにhttp://scikit-learn.org/stable/modules/grid_search.html#parallelism[grid search]がありますまたは keras + `)、マルチプロセッシングは明らかな選択です。

すべての関数がブロッキング呼び出しを使用する場合、すべての関数の前に単に「+ async +」を置くことは悪い考えです。 (これにより実際にコードが遅くなる可能性があります。)しかし、前述のように、非同期IOとマルチプロセッシングがhttps://youtu.be/0kXaLh8Fz3k?t=10m30s[live in harmony]できる場所があります。

非同期IOとスレッド化のコンテストは、もう少し直接的です。 はじめに、「スレッド化は難しい」と述べました。完全な話は、スレッド化を実装するのが簡単だと思われる場合でも、とりわけ、競合状態やメモリ使用量のために、悪名高いトレース不可能なバグにつながる可能性があるということです。

スレッドは有限の可用性を備えたシステムリソースであるため、スレッドは非同期IOよりもエレガントにスケーリングする傾向があります。 多くのマシンで数千のスレッドを作成すると失敗します。そもそも試してみることはお勧めしません。 数千の非同期IOタスクの作成は完全に実行可能です。

非同期IOは、複数のIOにバインドされたタスクがあり、タスクがそうでなければIOにバインドされた待機時間をブロックすることによってタスクが支配される場合に光ります。

  • プログラムがサーバー側かクライアント側かに関係なく、ネットワークIO

  • グループチャットルームのようなピアツーピアのマルチユーザーネットワークなどのサーバーレス設計

  • 「消して忘れる」スタイルを模倣したいが、読み書きするものをロックする心配はあまりない読み取り/書き込み操作

それを使用しない最大の理由は、 + await +`が特定のメソッドセットを定義する特定のオブジェクトセットのみをサポートすることです。 特定のDBMSで非同期読み取り操作を実行する場合は、そのDBMSのPythonラッパーだけでなく、 `+ async +/`+ await +`構文をサポートするラッパーを見つける必要があります。 同期呼び出しを含むコルーチンは、他のコルーチンおよびタスクの実行をブロックします。

+ async +/`+ await +`で動作するライブラリのリストについては、このチュートリアルの最後にあるリンク:#libraries-that-work-with-asyncawait [list]を参照してください。

非同期IOですが、どれですか?

このチュートリアルでは、非同期IO、 + async +/`+ await `構文、およびイベントループ管理とタスクの指定に ` asyncio `を使用することに焦点を当てています。 確かに、 ` asyncio +`は唯一の非同期IOライブラリではありません。 ナサニエルJからのこの観察 スミスはたくさん言います:

_ _ [In] a few years, asyncio might find itself relegated to becoming one of those stdlib libraries that savvy developers avoid, like urllib2.

私が実際に主張しているのは、「+ asyncio 」がそれ自体の成功の犠牲者であるということです。設計時には、可能な限り最良のアプローチを使用しました。しかしそれ以来、 ` asyncio `に触発された作業( ` async `/` await `の追加など)は、私たちがさらに良くできるようにランドスケープをシフトし、今では ` asyncio +`はその以前のコミットメントによって妨げられています。 (ソース) _ _

そのために、異なるAPIと異なるアプローチであるにもかかわらず、「+ asyncio 」が行うことを行ういくつかの有名な代替手段がhttps://github.com/dabeaz/curio [` curio `]およびhttps://です。 github.com/python-trio/trio [` trio `]。 個人的には、中規模の単純なプログラムを作成する場合は、「 asyncio +」を使用するだけで十分で理解しやすく、Pythonの標準ライブラリの外部に別の大きな依存関係を追加することを回避できます。

しかし、必ず、「+ curio 」と「 trio +」をチェックすると、ユーザーにとってより直感的な方法で同じことが実現されることがあります。 ここで紹介するパッケージに依存しない概念の多くは、代替の非同期IOパッケージにも浸透するはずです。

オッズと終了

これらの次のいくつかのセクションでは、これまでチュートリアルにうまく収まっていなかったが、完全なビルドと理解のためにまだ重要な、「+ asyncio 」と「 async + / + await + `のその他の部分をカバーします。プログラム。

他のトップレベルの `+ asyncio +`関数

`+ asyncio.run()`に加えて、 ` asyncio.create_task()`や ` asyncio.gather()+`などの他のパッケージレベルの関数をいくつか見てきました。

`+ create_task()`を使用してコルーチンオブジェクトの実行をスケジュールし、その後に ` asyncio.run()+`を続けることができます。

>>>

>>> import asyncio

>>> async def coro(seq) -> list:
...     """'IO' wait time is proportional to the max element."""
...     await asyncio.sleep(max(seq))
...     return list(reversed(seq))
...
>>> async def main():
...     # This is a bit redundant in the case of one task
...     # We could use `await coro([3, 2, 1])` on its own
...     t = asyncio.create_task(coro([3, 2, 1]))  # Python 3.7+
...     await t
...     print(f't: type {type(t)}')
...     print(f't done: {t.done()}')
...
>>> t = asyncio.run(main())
t: type <class '_asyncio.Task'>
t done: True

このパターンには微妙な点があります。「+ main()」内で「 await t 」を行わない場合、「 main()」自体が完了を通知する前に終了する可能性があります。 ` asyncio.run(main())` https://github.com/python/cpython/blob/7e18deef652a9d413d5dbd19d61073ba7eb5460e/Lib/asyncio/runners.py#L43[calls ` loop.run_until_complete(main())) + `]、イベントループは(+ await t `が存在しない場合)のみに関係し、 ` main()`内で作成されるタスクが実行されることではなく、 ` main()`が実行されることを意味します。 「 await t 」を使用しない場合、ループの他のタスクはhttps://github.com/python/cpython/blob/7e18deef652a9d413d5dbd19d61073ba7eb5460e/Lib/asyncio/runners.py#L46 [キャンセルされます]、おそらく完了する前に。 現在保留中のタスクのリストを取得する必要がある場合は、 ` asyncio.Task.all_tasks()+`を使用できます。

注意: `+ asyncio.create_task()`はPython 3.7で導入されました。 Python 3.6以前では、 ` create_task()`の代わりに ` asyncio.ensure_future()+`を使用します。

それとは別に、 `+ asyncio.gather()`があります。 特別なことは何も行いませんが、「 gather()」はコルーチン(未来)のコレクションを1つの未来にきちんと入れることを目的としています。 その結果、単一のfutureオブジェクトが返され、 ` await asyncio.gather()`で複数のタスクまたはコルーチンを指定すると、それらすべてが完了するのを待っています。 (これは前の例の ` queue.join()`に多少似ています。) ` gather()+`の結果は、入力全体の結果のリストになります。

>>>

>>> import time
>>> async def main():
...     t = asyncio.create_task(coro([3, 2, 1]))
...     t2 = asyncio.create_task(coro([10, 5, 0]))  # Python 3.7+
...     print('Start:', time.strftime('%X'))
...     a = await asyncio.gather(t, t2)
...     print('End:', time.strftime('%X'))  # Should be 10 seconds
...     print(f'Both tasks done: {all((t.done(), t2.done()))}')
...     return a
...
>>> a = asyncio.run(main())
Start: 16:20:11
End: 16:20:21
Both tasks done: True
>>> a
[[You probably noticed that `+gather()+` waits on the entire result set of the Futures or coroutines that you pass it. Alternatively, you can loop over `+asyncio.as_completed()+` to get tasks as they are completed, in the order of completion. The function returns an iterator that yields tasks as they finish. Below, the result of `+coro([3, 2, 1])+` will be available before `+coro([10, 5, 0])+` is complete, which is not the case with `+gather()+`:

[.repl-toggle]#>>>#

[source,python,repl]

>>> async def main(): . t = asyncio.create_task(coro([3、2、1])) . t2 = asyncio.create_task(coro([10、5、0])) . print( 'Start:'、time.strftime( '%X')) . asyncio.as_completed((t、t2))の解像度: . compl = resを待つ . print(f’res:{compl}は{time.strftime( "%X")} 'で完了しました) . print( 'End:'、time.strftime( '%X')) . print(f’Both tasks done:{all((t.done()、t2.done()))} ') …​ >>> a = asyncio.run(main())開始:09:49:07 res:[1、2、3]完了09:49:10 res:[0、5、10]完了09:49 :17終了:09:49:17完了した両方のタスク:True

Lastly, you may also see `+asyncio.ensure_future()+`. You should rarely need it, because it’s a lower-level plumbing API and largely replaced by `+create_task()+`, which was introduced later.

==== The Precedence of `+await+`

While they behave somewhat similarly, the `+await+` keyword has significantly higher precedence than `+yield+`. This means that, because it is more tightly bound, there are a number of instances where you’d need parentheses in a `+yield from+` statement that are not required in an analogous `+await+` statement. For more information, see https://www.python.org/dev/peps/pep-0492/#examples-of-await-expressions[examples of `+await+` expressions] from PEP 492.

=== Conclusion

You’re now equipped to use `+async+`/`+await+` and the libraries built off of it. Here’s a recap of what you’ve covered:

* Asynchronous IO as a language-agnostic model and a way to effect concurrency by letting coroutines indirectly communicate with each other
* The specifics of Python’s new `+async+` and `+await+` keywords, used to mark and define coroutines
* `+asyncio+`, the Python package that provides the API to run and manage coroutines

=== Resources

==== Python Version Specifics

Async IO in Python has evolved swiftly, and it can be hard to keep track of what came when. Here’s a list of Python minor-version changes and introductions related to `+asyncio+`:

* 3.3: The `+yield from+` expression allows for generator delegation.
* 3.4: `+asyncio+` was introduced in the Python standard library with provisional API status.
* 3.5: `+async+` and `+await+` became a part of the Python grammar, used to signify and wait on coroutines. They were not yet reserved keywords. (You could still define functions or variables named `+async+` and `+await+`.)
* 3.6: Asynchronous generators and asynchronous comprehensions were introduced. The API of `+asyncio+` was declared stable rather than provisional.
* 3.7: `+async+` and `+await+` became reserved keywords. (They cannot be used as identifiers.) They are intended to replace the `+asyncio.coroutine()+` decorator. `+asyncio.run()+` was introduced to the `+asyncio+` package, among https://docs.python.org/3/whatsnew/3.7.html#whatsnew37-asyncio[a bunch of other features].

If you want to be safe (and be able to use `+asyncio.run()+`), go with Python 3.7 or above to get the full set of features.

==== Articles

Here’s a curated list of additional resources:

* Real Python: https://realpython.com/python-concurrency/[Speed up your Python Program with Concurrency]
* Real Python: https://realpython.com/python-gil/[What is the Python Global Interpreter Lock?]
* CPython: The `+asyncio+` package https://github.com/python/cpython/tree/master/Lib/asyncio[source]
* Python docs: https://docs.python.org/3/reference/datamodel.html#coroutines[Data model > Coroutines]
* TalkPython: https://training.talkpython.fm/courses/details/async-in-python-with-threading-and-multiprocessing[Async Techniques and Examples in Python] https://github.com/talkpython/async-techniques-python-course[]
* Brett Cannon: https://snarky.ca/how-the-heck-does-async-await-work-in-python-3-5/[How the Heck Does Async-Await Work in Python 3.5?]
* PYMOTW: https://pymotw.com/3/asyncio/[`+asyncio+`]
* A. Jesse Jiryu Davis and Guido van Rossum: http://aosabook.org/en/500L/a-web-crawler-with-asyncio-coroutines.html[A Web Crawler With asyncio Coroutines]
* Andy Pearce: http://www.andy-pearce.com/blog/posts/2016/Jun/the-state-of-python-coroutines-yield-from/[The State of Python Coroutines: `+yield from+`]
* Nathaniel J. Smith: https://vorpus.org/blog/some-thoughts-on-asynchronous-api-design-in-a-post-asyncawait-world/[Some Thoughts on Asynchronous API Design in a Post-`+async+`/`+await+` World]
* Armin Ronacher: http://lucumr.pocoo.org/2016/10/30/i-dont-understand-asyncio/[I don’t understand Python’s Asyncio]
* Andy Balaam: http://www.artificialworlds.net/blog/2017/05/31/basic-ideas-of-python-3-asyncio-concurrency/[series on `+asyncio+`] (4 posts)
* Stack Overflow: https://stackoverflow.com/q/40836800/7954504[Python `+asyncio.semaphore+` in `+async+`-`+await+` function]
* Yeray Diaz:
** https://hackernoon.com/asyncio-for-the-working-python-developer-5c468e6e2e8e[AsyncIO for the Working Python Developer]
** https://medium.com/python-pandemonium/asyncio-coroutine-patterns-beyond-await-a6121486656f[Asyncio Coroutine Patterns: Beyond `+await+`]

A few Python _What’s New_ sections explain the motivation behind language changes in more detail:

* https://docs.python.org/3/whatsnew/3.3.html#pep-380[What’s New in Python 3.3] (`+yield from+` and PEP 380)
* https://docs.python.org/3/whatsnew/3.6.html#whatsnew36-pep525[What’s New in Python 3.6] (PEP 525 & 530)

From David Beazley:

* http://www.dabeaz.com/generators/[Generator: Tricks for Systems Programmers]
* http://www.dabeaz.com/coroutines/[A Curious Course on Coroutines and Concurrency]
* http://dabeaz.com/finalgenerator/index.html[Generators: The Final Frontier]

YouTube talks:

* https://youtu.be/0kXaLh8Fz3k[John Reese - Thinking Outside the GIL with AsyncIO and Multiprocessing - PyCon 2018]
* https://youtu.be/ZzfHjytDceU[Keynote David Beazley - Topics of Interest (Python Asyncio)]
* https://youtu.be/MCs5OvhV9S4[David Beazley - Python Concurrency From the Ground Up: LIVE! - PyCon 2015]
* https://youtu.be/9zinZmE3Ogk[Raymond Hettinger, Keynote on Concurrency, PyBay 2017]
* https://youtu.be/Bv25Dwe84g0[Thinking about Concurrency, Raymond Hettinger, Python core developer]
* https://youtu.be/iG6fr81xHKA[Miguel Grinberg Asynchronous Python for the Complete Beginner PyCon 2017]
* https://youtu.be/2ZFFv-wZ8_g[Yury Selivanov asyncawait and asyncio in Python 3 6 and beyond PyCon 2017]
* https://youtu.be/E-1Y4kSsAFc[Fear and Awaiting in Async: A Savage Journey to the Heart of the Coroutine Dream]
* https://youtu.be/kdzL3r-yJZY[What Is Async, How Does It Work, and When Should I Use It? (PyCon APAC 2014)]

==== Related PEPs

[cols=",",options="header",]
|===
|PEP |Date Created
|https://www.python.org/dev/peps/pep-0342/[PEP 342 – Coroutines via Enhanced Generators] |2005-05
|https://www.python.org/dev/peps/pep-0380/[PEP 380 – Syntax for Delegating to a Subgenerator] |2009-02
|https://www.python.org/dev/peps/pep-3153/[PEP 3153 – Asynchronous IO support] |2011-05
|https://www.python.org/dev/peps/pep-3156/[PEP 3156 – Asynchronous IO Support Rebooted: the “asyncio” Module] |2012-12
|https://www.python.org/dev/peps/pep-0492/[PEP 492 – Coroutines with async and await syntax] |2015-04
|https://www.python.org/dev/peps/pep-0525/[PEP 525 – Asynchronous Generators] |2016-07
|https://www.python.org/dev/peps/pep-0530/[PEP 530 – Asynchronous Comprehensions] |2016-09
|===

==== Libraries That Work With `+async+`/`+await+`

From https://github.com/aio-libs[aio-libs]:

* https://github.com/aio-libs/aiohttp[`+aiohttp+`]: Asynchronous HTTP client/server framework
* https://github.com/aio-libs/aioredis[`+aioredis+`]: Async IO Redis support
* https://github.com/aio-libs/aiopg[`+aiopg+`]: Async IO PostgreSQL support
* https://github.com/aio-libs/aiomcache[`+aiomcache+`]: Async IO memcached client
* https://github.com/aio-libs/aiokafka[`+aiokafka+`]: Async IO Kafka client
* https://github.com/aio-libs/aiozmq[`+aiozmq+`]: Async IO ZeroMQ support
* https://github.com/aio-libs/aiojobs[`+aiojobs+`]: Jobs scheduler for managing background tasks
* https://github.com/aio-libs/async_lru[`+async_lru+`]: Simple LRU cache for async IO

From https://magic.io/[magicstack]:

* https://github.com/MagicStack/uvloop[`+uvloop+`]: Ultra fast async IO event loop
* https://github.com/MagicStack/asyncpg[`+asyncpg+`]: (Also very fast) async IO PostgreSQL support

From other hosts:

* https://github.com/python-trio/trio[`+trio+`]: Friendlier `+asyncio+` intended to showcase a radically simpler design
* https://github.com/Tinche/aiofiles[`+aiofiles+`]: Async file IO
* https://github.com/theelous3/asks[`+asks+`]: Async requests-like http library
* https://github.com/jonathanslenders/asyncio-redis[`+asyncio-redis+`]: Async IO Redis support
* https://github.com/dano/aioprocessing[`+aioprocessing+`]: Integrates `+multiprocessing+` module with `+asyncio+`
* https://github.com/Scille/umongo[`+umongo+`]: Async IO MongoDB client
* https://github.com/alex-sherman/unsync[`+unsync+`]: Unsynchronize `+asyncio+`
* https://github.com/vxgmichel/aiostream[`+aiostream+`]: Like `+itertools+`, but async