asyncioは実際にどのように機能しますか?


118

この質問は、別の質問「cdefで待機する方法」によって動機付けられています。

Webにはに関する記事やブログ投稿がたくさんありますがasyncio、それらはすべて表面的なものです。asyncio実際にどのように実装されているのか、I / Oが非同期になる原因についての情報は見つかりませんでした。私はソースコードを読み込もうとしていましたが、何千行もの最高級のCコードではありません。その多くは補助オブジェクトを扱っていますが、最も重要なのは、Python構文とそれが変換するCコードとを関連付けるのが難しいことです。に。

Asycnio独自のドキュメントは、あまり役に立ちません。それがどのように機能するかについての情報はそこにはありません、それを使用する方法についてのいくつかのガイドラインだけがあり、これも時々誤解を招く/非常に不十分に書かれています。

私はGoのコルーチンの実装に精通しており、Pythonが同じことを実行することを望んでいました。その場合、上記のリンクで私が思いついたコードはうまくいきました。それができなかったので、私は今、その理由を理解しようとしています。これまでの私の推測は次のとおりです。間違っている箇所を修正してください。

  1. フォームのプロシージャ定義は、async def foo(): ...実際にはクラスを継承するメソッドとして解釈されますcoroutine
  2. おそらく、async def実際にはawaitステートメントによって複数のメソッドに分割され、これらのメソッドが呼び出されるオブジェクトは、これまでの実行を通じて行われた進行状況を追跡できます。
  3. 上記が真の場合、基本的に、コルーチンの実行は、いくつかのグローバルマネージャー(ループ?)によるコルーチンオブジェクトのメソッドの呼び出しに要約されます。
  4. グローバルマネージャーは、I / O操作がPython(のみ?)コードによっていつ実行されるかをどうにか(方法?)認識しており、現在実行中のメソッドが制御を放棄した後に実行する保留中のコルーチンメソッドの1つを選択できます(awaitステートメントのヒット))。

言い換えると、これは、いくつかのasyncio構文をよりわかりやすいものに「脱糖」するための私の試みです。

async def coro(name):
    print('before', name)
    await asyncio.sleep()
    print('after', name)

asyncio.gather(coro('first'), coro('second'))

# translated from async def coro(name)
class Coro(coroutine):
    def before(self, name):
        print('before', name)

    def after(self, name):
        print('after', name)

    def __init__(self, name):
        self.name = name
        self.parts = self.before, self.after
        self.pos = 0

    def __call__():
        self.parts[self.pos](self.name)
        self.pos += 1

    def done(self):
        return self.pos == len(self.parts)


# translated from asyncio.gather()
class AsyncIOManager:

    def gather(*coros):
        while not every(c.done() for c in coros):
            coro = random.choice(coros)
            coro()

私の推測が正しいことが証明されたら、問題が発生します。このシナリオでは、実際にI / Oはどのように行われますか?別のスレッドで?インタプリタ全体が一時停止され、I / Oはインタプリタの外部で行われますか?I / Oとはどういう意味ですか?私のpythonプロシージャがCプロシージャを呼び出しopen()、それがカーネルに割り込みを送り、それに制御を放棄した場合、Pythonインタープリターはこれについてどのように認識し、カーネルコードが実際のI / Oを実行しながら、他のコードの実行を継続できますか?それは最初に割り込みを送ったPythonプロシージャを起こしますか?原則としてPythonインタープリターはどのようにしてこれが起こっているのかを知ることができますか?


2
ほとんどのロジックは、イベントループの実装によって処理されます。どのようにCPythonのを見てみると、BaseEventLoop実装されていますgithub.com/python/cpython/blob/...
ブレンダーを

@Blenderわかりました、私は最終的に自分が欲しかったものを見つけたと思いますが、今はコードがそのように書かれた理由がわかりません。なぜ_run_once、このモジュール全体では実際にのみ有効な機能ですが、「プライベート」作られ、?実装は恐ろしいですが、それはそれほど問題ではありません。イベントループで呼び出したい関数が「私を呼び出さないでください」とマークされているのはなぜですか?
wvxvw

それはメーリングリストへの質問です。_run_once最初に触れる必要があるのはどのユースケースですか?
Blender

8
しかし、それでも私の質問には答えられません。どのようにして、有用な問題を単に解決します_run_onceか?asyncioは複雑であり、欠点がありますが、議論は非公開にしてください。あなた自身が理解していないコードの背後にある開発者を悪口を言わないでください。
Blender

1
@ user8371915私がカバーしていないことがあると思われる場合は、私の答えを追加またはコメントしてください。
バレル

回答:


202

asyncioはどのように機能しますか?

この質問に答える前に、いくつかの基本用語を理解する必要があります。すでに知っている用語がある場合は、これらをスキップしてください。

発電機

ジェネレーターは、Python関数の実行を一時停止できるようにするオブジェクトです。ユーザーが作成したジェネレーターは、キーワードを使用して実装されyieldます。yieldキーワードを含む通常の関数を作成することにより、その関数をジェネレーターに変換します。

>>> def test():
...     yield 1
...     yield 2
...
>>> gen = test()
>>> next(gen)
1
>>> next(gen)
2
>>> next(gen)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration

ご覧のとおりnext()、ジェネレーターを呼び出すと、インタープリターがテストのフレームをロードし、yielded値を返します。next()もう一度呼び出すと、フレームがインタプリタスタックに再度読み込まれ、yield別の値の処理が続行されます。

3回目にnext()呼ばれるまでに、ジェネレーターは完成し、StopIteration投げられました。

ジェネレーターと通信する

ジェネレーターのあまり知られていない機能は、2つの方法を使用してそれらと通信できるという事実です:send()throw()

>>> def test():
...     val = yield 1
...     print(val)
...     yield 2
...     yield 3
...
>>> gen = test()
>>> next(gen)
1
>>> gen.send("abc")
abc
2
>>> gen.throw(Exception())
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 4, in test
Exception

を呼び出すgen.send()と、値はyieldキーワードからの戻り値として渡されます。

gen.throw()一方、ジェネレーター内で例外をスローすることを許可します。例外yieldは、呼び出された同じ場所で発生します。

ジェネレータから値を返す

ジェネレータから値を返すと、その値はStopIteration例外の内部に置かれます。後で例外から値を回復し、必要に応じて使用できます。

>>> def test():
...     yield 1
...     return "abc"
...
>>> gen = test()
>>> next(gen)
1
>>> try:
...     next(gen)
... except StopIteration as exc:
...     print(exc.value)
...
abc

見よ、新しいキーワード: yield from

Python 3.4には、新しいキーワードが追加されましたyield from。キーワードは私たちが行うことができますことを、任意の上のパスは何ですかnext()send()そしてthrow()最も内側のネストされた発電機へ。内部ジェネレータが値を返す場合、それは次の戻り値でもありますyield from

>>> def inner():
...     inner_result = yield 2
...     print('inner', inner_result)
...     return 3
...
>>> def outer():
...     yield 1
...     val = yield from inner()
...     print('outer', val)
...     yield 4
...
>>> gen = outer()
>>> next(gen)
1
>>> next(gen) # Goes inside inner() automatically
2
>>> gen.send("abc")
inner abc
outer 3
4

このトピックについてさらに詳しく説明する記事を書きまし

すべてを一緒に入れて

yield fromPython 3.4で新しいキーワードを導入すると、トンネルのようにジェネレーター内にジェネレーターを作成し、最も内側のジェネレーターから最も外側のジェネレーターにデータをやり取りできるようになりました。これにより、ジェネレーター- コルーチンに新しい意味が生まれました。

コルーチンは、実行中に停止および再開できる関数です。Pythonでは、async defキーワードを使用して定義されます。ジェネレータのように、それらも独自の形式でyield fromあるを使用しawaitます。Python 3.5で導入された以前はasyncawaitジェネレーターを作成したのとまったく同じ方法で(のyield from代わりにawait)コルーチンを作成していました。

async def inner():
    return 1

async def outer():
    await inner()

__iter__()メソッドを実装するすべてのイテレーターまたはジェネレーターと同様に、コルーチンは、呼び出される__await__()たびに継続できるコルーチンを実装しますawait coro

Pythonのドキュメント内に、チェックする必要がある優れたシーケンス図があります。

asyncioでは、コルーチン関数とは別に、2つの重要なオブジェクトがあります。タスクフューチャーです。

先物

フューチャーは、__await__()メソッドが実装されたオブジェクトであり、その役割は特定の状態と結果を保持することです。状態は次のいずれかです。

  1. PENDING-futureには結果または例外が設定されていません。
  2. CANCELED-将来はキャンセルされました fut.cancel()
  3. FINISHED-結果セットを使用するfut.set_result()か、例外セットを使用して、futureは終了しましたfut.set_exception()

結果は、ご想像のとおり、返されるPythonオブジェクトか、発生する可能性のある例外のいずれかです。

オブジェクトのもう1つの重要な機能は、futureオブジェクトにというメソッドが含まれていることadd_done_callback()です。このメソッドを使用すると、例外が発生したか終了したかに関係なく、タスクが完了するとすぐに関数を呼び出すことができます。

タスク

タスクオブジェクトは、コルーチンをラップし、最も内側のコルーチンと最も外側のコルーチンと通信する特別なフューチャーです。コルーチンがawaitフューチャーになるたびに、フューチャーは(のようにyield from)タスクにずっと渡され、タスクはそれを受け取ります。

次に、タスクは未来にバインドします。それはadd_done_callback()未来を求めることによってそうします。今後、キャンセルされるか、例外が渡されるか、結果としてPythonオブジェクトが渡されるかのいずれかで将来が行われる場合、タスクのコールバックが呼び出され、存在が回復します。

非同期

私たちが答えなければならない最後の書き込みの質問は-IOはどのように実装されているのですか?

asyncioの内部には、イベントループがあります。タスクのイベントループ。イベントループの仕事は、準備ができるたびにタスクを呼び出し、そのすべての作業を1台の作業用マシンに調整することです。

イベントループのIO部分は、という単一の重要な関数に基づいて構築されていselectます。Selectは、下にあるオペレーティングシステムによって実装されるブロッキング関数であり、着信データまたは発信データをソケットで待機できるようにします。データが受信されるとウェイクアップし、データを受信したソケット、または書き込みの準備ができているソケットを返します。

asyncioを介してソケットを介してデータを送受信しようとすると、実際に以下で実際に発生するのは、ソケットがすぐに読み取りまたは送信できるデータがあるかどうかが最初にチェックされることです。.send()バッファがいっぱいの場合、または.recv()バッファが空の場合、ソケットはselect関数に登録され(rlistfor recvおよびwlistforのリストの1つに単純に追加されますsend)、適切な関数がそのソケットに関連付けられたawait新しく作成されたfutureオブジェクトに登録されます。

利用可能なすべてのタスクが先物を待っているときに、イベントループを呼び出しselectて待機。ソケットの1つに着信データがあるか、そのsendバッファーが空になると、asyncioはそのソケットに関連付けられている将来のオブジェクトをチェックし、それを完了に設定します。

今、すべての魔法が起こります。未来が完了し、以前に追加されたタスクが復活し、最も内側のコルーチンを再開するコルーチンをadd_done_callback()呼び出し.send()awaitチェーンのため)、新しく受信したデータを近くのバッファーから読み取りますにこぼれた。

再びメソッドチェーンrecv()

  1. select.select 待ちます。
  2. データを含む準備ができたソケットが返されます。
  3. ソケットからのデータはバッファに移動されます。
  4. future.set_result() と呼ばれます。
  5. で自身を追加タスクをadd_done_callback()今すぐウェイクアップされます。
  6. タスクは.send()、最も内側のコルーチンにずっと入り込み、それを起こすコルーチンを呼び出します。
  7. データはバッファから読み取られ、謙虚なユーザーに返されます。

要約すると、asyncioはジェネレーター機能を使用して、関数の一時停止と再開を可能にします。yield from最も内側のジェネレータから最も外側のジェネレータにデータをやり取りできる機能を使用します。IOが完了するのを(OS select関数を使用して)待機している間、関数の実行を停止するためにこれらすべてを使用します。

そして何よりも ある機能が一時停止している間に、別の機能が実行され、asyncioである繊細なファブリックとインターリーブする場合があります。


12
他に説明が必要な場合は、遠慮なくコメントしてください。ところで、これをブログの記事として書いたのか、stackoverflowの回答として書いたのかはよくわかりません。質問は答えるのが長いです。
バレル

1
非同期ソケットでは、データを送受信しようとすると、最初にOSバッファーがチェックされます。受信しようとしていて、バッファにデータがない場合、基になる受信関数はエラー値を返し、Pythonの例外として伝播します。送信およびフルバッファと同じです。例外が発生すると、Pythonはそれらのソケットを、プロセスを中断するselect関数に送信します。しかし、それはasyncioがどのように動作するかではなく、selectとsocketsがどのように動作するかであり、これもOSに固有のものです。
バレル2018

2
@ user8371915いつもここにいます:-) Asyncioを理解するためには、ジェネレーター、ジェネレーターの通信方法、および動作方法を知っている必要があることに注意してくださいyield from。ただし、読者がすでにそれを知っている場合はスキップできることを上に書き留めました:-)他に追加する必要があると思われるものはありますか?
バレル

2
Asyncio セクションの前の事柄は、言語がそれ自体で実際に行う唯一のことなので、おそらく最も重要です。selectそれは、ノンブロッキングI / Oシステムは、OS上で作業を呼び出す方法であることから、同様の資格があります。実際のasyncio構成とイベントループは、これらのものから構築されたアプリレベルのコードにすぎません。
MisterMiyagi 2018

3
この投稿には、Pythonの非同期I / Oのバックボーンに関する情報があります。そのような親切な説明をありがとう。
mjkim 2018

83

話すことasync/awaitasyncio同じことではありません。最初のものは基本的な低レベルの構成要素(コルーチン)であり、後者はこれらの構成要素を使用するライブラリです。逆に、単一の最終的な答えはありません。

以下は、async/awaitおよび- asyncioライクなライブラリの動作の概要です。つまり、上に他のトリックがあるかもしれません(ある...)が、自分で構築しない限り、重要ではありません。あなたがそのような質問をする必要がないほど十分に知っているのでない限り、違いは無視できるはずです。

1.コルーチン対ナッツ殻のサブルーチン

同じようにサブルーチン(関数、プロシージャ、...)、コルーチンコード片を実行するスタックがあり、それぞれが特定の命令である:(ジェネレータは、...)コールスタックと命令ポインタの抽象化です。

def対の区別async defは、単に明確にするためです。実際の違いはreturnyieldです。これから、awaitまたはyield from個々の呼び出しとスタック全体の違いを理解してください。

1.1。サブルーチン

サブルーチンは、ローカル変数を保持するための新しいスタックレベルと、最後に到達するためのその命令の単一のトラバーサルを表します。次のようなサブルーチンについて考えてみます。

def subfoo(bar):
     qux = 3
     return qux * bar

実行すると、それは

  1. barおよびにスタックスペースを割り当てるqux
  2. 最初のステートメントを再帰的に実行し、次のステートメントにジャンプします
  3. 一度、returnその値を呼び出しスタックにプッシュします
  4. スタック(1.)と命令ポインタ(2.)をクリアします。

特に、4。は、サブルーチンが常に同じ状態で開始されることを意味します。関数自体に排他的なものはすべて、完了すると失われます。の後に指示があっても、機能を再開することはできませんreturn

root -\
  :    \- subfoo --\
  :/--<---return --/
  |
  V

1.2。永続的なサブルーチンとしてのコルーチン

コルーチンはサブルーチンのようなものですが、その状態破壊することなく終了できます。このようなコルーチンを考えてみましょう:

 def cofoo(bar):
      qux = yield bar  # yield marks a break point
      return qux

実行すると、それは

  1. barおよびにスタックスペースを割り当てるqux
  2. 最初のステートメントを再帰的に実行し、次のステートメントにジャンプします
    1. 一度、yieldその値を呼び出しスタックにプッシュしますが、スタックと命令ポインタを保存します
    2. を呼び出すとyield、スタックと命令ポインタを復元し、引数をqux
  3. 一度、returnその値を呼び出しスタックにプッシュします
  4. スタック(1.)と命令ポインタ(2.)をクリアします。

2.1と2.2の追加に注意してください。コルーチンは、事前定義されたポイントで中断および再開できます。これは、別のサブルーチンの呼び出し中にサブルーチンが中断される方法に似ています。違いは、アクティブなコルーチンが呼び出しスタックに厳密にバインドされていないことです。代わりに、中断されたコルーチンは、別の分離されたスタックの一部です。

root -\
  :    \- cofoo --\
  :/--<+--yield --/
  |    :
  V    :

つまり、中断されたコルーチンは、スタック間で自由に保管または移動できます。コルーチンにアクセスできる呼び出しスタックは、コルーチンを再開することを決定できます。

1.3。呼び出しスタックをたどる

これまでのところ、私たちのコルーチンは、でのみコールスタックを下りますyield。サブルーチンがダウンして行くことができ、最大でコールスタックreturn()。完全を期すために、コルーチンには、コールスタックを上げるメカニズムも必要です。このようなコルーチンを考えてみましょう:

def wrap():
    yield 'before'
    yield from cofoo()
    yield 'after'

それを実行すると、それはまだサブルーチンのようにスタックと命令ポインタを割り当てることを意味します。一時停止しても、それはまだサブルーチンを格納するようなものです。

しかし、yield fromありません両方。スタックと命令ポインタを一時停止wrap して実行しcofooます。が完全に終了するwrapまで中断されたままになることに注意してくださいcofoo。たびcofoo一時停止または何かが送信され、cofoo直接呼び出しスタックに接続されています。

1.4。ずっと下のコルーチン

確立されたとおり、yield from2つのスコープを別の中間スコープに接続できます。再帰的に適用すると、スタックの上部をスタックの下部に接続できます。

root -\
  :    \-> coro_a -yield-from-> coro_b --\
  :/ <-+------------------------yield ---/
  |    :
  :\ --+-- coro_a.send----------yield ---\
  :                             coro_b <-/

お互いに知らないことに注意しrootcoro_bください。これにより、コルーチンはコールバックよりもはるかにクリーンになります。コルーチンは依然として、サブルーチンのような1対1の関係に基づいて構築されています。コルーチンは、通常の呼び出しポイントまで、既存の実行スタック全体を一時停止および再開します。

特に、root再開するコルーチンの数は任意です。しかし、同時に複数を再開することはできません。同じルートのコルーチンは並行ですが、並列ではありません!

1.5。Python asyncawait

説明ではこれまで明示的にジェネレータのyieldyield fromボキャブラリを使用してきました-基本的な機能は同じです。新しいPython3.5構文はasyncawait主に明確にするために存在します。

def foo():  # subroutine?
     return None

def foo():  # coroutine?
     yield from foofoo()  # generator? coroutine?

async def foo():  # coroutine!
     await foofoo()  # coroutine!
     return None

async forそしてasync withあなたは破るためのステートメントが必要とされてyield from/await裸でチェーンforwith文を。

2.単純なイベントループの構造

コルーチン自体には、別のコルーチンを制御するという概念はありません。コルーチンスタックの最下部にある呼び出し元にのみ制御を渡すことができます。この呼び出し元は、別のコルーチンに切り替えて実行できます。

いくつかのコルーチンのこのルートノードは通常、イベントループです。一時停止すると、コルーチンは再開したいイベントを生成します。次に、イベントループは、これらのイベントの発生を効率的に待機できます。これにより、次に実行するコルーチン、または再開する前に待機する方法を決定できます。

このような設計は、ループが理解する一連の定義済みイベントがあることを意味します。await最終的にイベントがawait編集されるまで、いくつかのコルーチンが互いに。このイベントは、制御を行うことにより、イベントループと直接通信できyieldます。

loop -\
  :    \-> coroutine --await--> event --\
  :/ <-+----------------------- yield --/
  |    :
  |    :  # loop waits for event to happen
  |    :
  :\ --+-- send(reply) -------- yield --\
  :        coroutine <--yield-- event <-/

重要なのは、コルーチンの中断により、イベントループとイベントが直接通信できるようになることです。中間コルーチンスタックは、どのループがそれを実行しているか、イベントがどのように機能しているかについての知識を必要としません。

2.1.1。時間内のイベント

処理する最も簡単なイベントは、ある時点に到達することです。これは、スレッド化されたコードの基本的なブロックでもsleepあります。スレッドは、条件がtrueになるまで繰り返しsを実行します。ただし、通常はsleepそれ自体で実行をブロックします。他のコルーチンはブロックされないようにします。代わりに、現在のコルーチンスタックを再開するタイミングをイベントループに伝えます。

2.1.2。イベントの定義

イベントは、列挙型、タイプ、またはその他のIDを介して識別できる単なる値です。これは、ターゲット時間を格納する単純なクラスで定義できます。イベント情報を保存するだけでなくawait、クラスに直接許可することもできます。

class AsyncSleep:
    """Event to sleep until a point in time"""
    def __init__(self, until: float):
        self.until = until

    # used whenever someone ``await``s an instance of this Event
    def __await__(self):
        # yield this Event to the loop
        yield self

    def __repr__(self):
        return '%s(until=%.1f)' % (self.__class__.__name__, self.until)

このクラスはイベントを保存するだけで、実際の処理方法は示していません。

唯一の特別な機能は__await__- awaitキーワードが検索するものです。実際には、イテレータですが、通常の反復機構では使用できません。

2.2.1。イベント待ち

イベントができたので、コルーチンはそれにどのように反応しますか?私たちは、同等のものを表現することができるはずsleepawait私たちのイベントをする。何が起こっているのかをよりよく理解するために、半分の時間を2回待機します。

import time

async def asleep(duration: float):
    """await that ``duration`` seconds pass"""
    await AsyncSleep(time.time() + duration / 2)
    await AsyncSleep(time.time() + duration / 2)

このコルーチンを直接インスタンス化して実行できます。ジェネレータと同様に、を使用coroutine.sendすると、yield結果が出るまでコルーチンが実行されます。

coroutine = asleep(100)
while True:
    print(coroutine.send(None))
    time.sleep(0.1)

これにより、2つのAsyncSleepイベントが生成され、次にStopIterationコルーチンが実行されます。唯一の遅延はtime.sleepループからのものであることに注意してください!それぞれAsyncSleep、現在時刻からのオフセットのみを保存します。

2.2.2。イベント+睡眠

この時点で、2つの異なるメカニズムを自由に使用できます。

  • AsyncSleep コルーチン内から生成できるイベント
  • time.sleep コルーチンに影響を与えずに待機できる

特に、これら2つは直交しています。一方が他方に影響を与えたり、トリガーしたりすることはありません。その結果、sleepの遅延に対応するための独自の戦略を立てることができますAsyncSleep

2.3。単純なイベントループ

複数のコルーチンがある場合、それぞれがいつ起きたいかを教えてくれます。その後、最初のユーザーが再開するまで、次に再開するまで待機します。特に、各ポイントで気になるのは次のどれかです。

これにより、簡単なスケジューリングが可能になります。

  1. コルーチンを希望する起床時間でソートする
  2. 目を覚ますしたい最初のものを選びます
  3. この時点まで待つ
  4. このコルーチンを実行する
  5. 1から繰り返します。

簡単な実装では、高度な概念は必要ありません。A listでは、コルーチンを日付でソートできます。待つのは常連time.sleep。コルーチンの実行は、以前と同じように機能しcoroutine.sendます。

def run(*coroutines):
    """Cooperatively run all ``coroutines`` until completion"""
    # store wake-up-time and coroutines
    waiting = [(0, coroutine) for coroutine in coroutines]
    while waiting:
        # 2. pick the first coroutine that wants to wake up
        until, coroutine = waiting.pop(0)
        # 3. wait until this point in time
        time.sleep(max(0.0, until - time.time()))
        # 4. run this coroutine
        try:
            command = coroutine.send(None)
        except StopIteration:
            continue
        # 1. sort coroutines by their desired suspension
        if isinstance(command, AsyncSleep):
            waiting.append((command.until, coroutine))
            waiting.sort(key=lambda item: item[0])

もちろん、これには改善の余地が十分あります。待機キューのヒープまたはイベントのディスパッチテーブルを使用できます。から戻り値をフェッチStopIterationして、コルーチンに割り当てることもできます。ただし、基本的な原則は変わりません。

2.4。協力待機

AsyncSleepイベントとrunイベントループは、時限イベントの完全に取り組んで実装されています。

async def sleepy(identifier: str = "coroutine", count=5):
    for i in range(count):
        print(identifier, 'step', i + 1, 'at %.2f' % time.time())
        await asleep(0.1)

run(*(sleepy("coroutine %d" % j) for j in range(5)))

これにより、5つのコルーチンのそれぞれが協調して切り替わり、それぞれを0.1秒間停止します。イベントループは同期ですが、作業は2.5秒ではなく0.5秒で実行されます。各コルーチンは状態を保持し、独立して機能します。

3. I / Oイベントループ

をサポートするイベントループsleepは、ポーリングに適しています。ただし、ファイルハンドルでのI / Oの待機はより効率的に行うことができます。オペレーティングシステムはI / Oを実装しているため、どのハンドルの準備が整っているかを認識しています。理想的には、イベントループは明示的な「I / O準備完了」イベントをサポートする必要があります。

3.1。selectコール

Pythonには、OSに読み取りI / Oハンドルを照会するためのインターフェースがすでにあります。読み取りまたは書き込みのハンドルを指定して呼び出されると、読み取りまたは書き込みの準備できたハンドル返されます。

readable, writeable, _ = select.select(rlist, wlist, xlist, timeout)

たとえばopen、書き込み用のファイルを作成して、準備が整うのを待ちます。

write_target = open('/tmp/foo')
readable, writeable, _ = select.select([], [write_target], [])

selectが戻ると、writeable開いているファイルが含まれます。

3.2。基本的なI / Oイベント

AsyncSleepリクエストと同様に、I / Oのイベントを定義する必要があります。基本的なselectロジックでは、イベントは読み取り可能なオブジェクト(openファイルなど)を参照する必要があります。また、読み取るデータの量を保存します。

class AsyncRead:
    def __init__(self, file, amount=1):
        self.file = file
        self.amount = amount
        self._buffer = ''

    def __await__(self):
        while len(self._buffer) < self.amount:
            yield self
            # we only get here if ``read`` should not block
            self._buffer += self.file.read(1)
        return self._buffer

    def __repr__(self):
        return '%s(file=%s, amount=%d, progress=%d)' % (
            self.__class__.__name__, self.file, self.amount, len(self._buffer)
        )

同じようにAsyncSleep、私たちほとんどがちょうど背後のシステムコールに必要なデータを格納します。今回__await__は、複数回再開することができます-私たちの希望amountが読み取られるまで。さらに、return単に再開するのではなく、I / Oの結果です。

3.3。読み取りI / Oによるイベントループの拡張

イベントループの基礎はrun以前に定義されたままです。まず、読み取りリクエストを追跡する必要があります。これはソートされたスケジュールではなく、読み取りリクエストをコルーチンにマップするだけです。

# new
waiting_read = {}  # type: Dict[file, coroutine]

select.selectはタイムアウトパラメータを取るので、の代わりに使用できますtime.sleep

# old
time.sleep(max(0.0, until - time.time()))
# new
readable, _, _ = select.select(list(reads), [], [])

これにより、すべての読み取り可能なファイルが得られます。ファイルがある場合は、対応するコルーチンを実行します。何もない場合は、現在のコルーチンが実行されるまで十分に待機しています。

# new - reschedule waiting coroutine, run readable coroutine
if readable:
    waiting.append((until, coroutine))
    waiting.sort()
    coroutine = waiting_read[readable[0]]

最後に、実際に読み取りリクエストをリッスンする必要があります。

# new
if isinstance(command, AsyncSleep):
    ...
elif isinstance(command, AsyncRead):
    ...

3.4。それを一緒に入れて

上記は少し単純化したものです。いつでも読むことができるのであれば、眠っているコルーチンを飢えさせないように切り替えを行う必要があります。読むものも待つものもないものを扱う必要があります。ただし、最終結果は30 LOCに収まります。

def run(*coroutines):
    """Cooperatively run all ``coroutines`` until completion"""
    waiting_read = {}  # type: Dict[file, coroutine]
    waiting = [(0, coroutine) for coroutine in coroutines]
    while waiting or waiting_read:
        # 2. wait until the next coroutine may run or read ...
        try:
            until, coroutine = waiting.pop(0)
        except IndexError:
            until, coroutine = float('inf'), None
            readable, _, _ = select.select(list(waiting_read), [], [])
        else:
            readable, _, _ = select.select(list(waiting_read), [], [], max(0.0, until - time.time()))
        # ... and select the appropriate one
        if readable and time.time() < until:
            if until and coroutine:
                waiting.append((until, coroutine))
                waiting.sort()
            coroutine = waiting_read.pop(readable[0])
        # 3. run this coroutine
        try:
            command = coroutine.send(None)
        except StopIteration:
            continue
        # 1. sort coroutines by their desired suspension ...
        if isinstance(command, AsyncSleep):
            waiting.append((command.until, coroutine))
            waiting.sort(key=lambda item: item[0])
        # ... or register reads
        elif isinstance(command, AsyncRead):
            waiting_read[command.file] = coroutine

3.5。協調I / O

AsyncSleepAsyncReadおよびrun実装は今、睡眠および/または読み取りに完全に機能しています。と同じようにsleepy、読み取りをテストするヘルパーを定義できます。

async def ready(path, amount=1024*32):
    print('read', path, 'at', '%d' % time.time())
    with open(path, 'rb') as file:
        result = return await AsyncRead(file, amount)
    print('done', path, 'at', '%d' % time.time())
    print('got', len(result), 'B')

run(sleepy('background', 5), ready('/dev/urandom'))

これを実行すると、I / Oが待機中のタスクとインターリーブされていることがわかります。

id background round 1
read /dev/urandom at 1530721148
id background round 2
id background round 3
id background round 4
id background round 5
done /dev/urandom at 1530721148
got 1024 B

4.非ブロッキングI / O

I / Oファイルには全体のコンセプトを取得しますが、それは次のようにライブラリのために本当に適していませんasyncioselect呼び出しは常にファイルに対して返し、両方openread無期限にブロック。これは、イベントループのすべてのコルーチンをブロックします-これは悪いことです。のようなライブラリはaiofiles、スレッドと同期を使用して、ファイル上の非ブロッキングI / Oとイベントを偽装します。

ただし、ソケットはノンブロッキングI / Oを許可します。ソケットには固有のレイテンシがあり、非常に重要です。イベントループで使用すると、データを待機して再試行することで、何もブロックせずにラップできます。

4.1。非ブロッキングI / Oイベント

と同様に、AsyncReadソケットの中断および読み取りイベントを定義できます。ファイルを取得する代わりに、ソケットを取得します-これは非ブロッキングでなければなりません。また、の代わりに__await__使用しsocket.recvますfile.read

class AsyncRecv:
    def __init__(self, connection, amount=1, read_buffer=1024):
        assert not connection.getblocking(), 'connection must be non-blocking for async recv'
        self.connection = connection
        self.amount = amount
        self.read_buffer = read_buffer
        self._buffer = b''

    def __await__(self):
        while len(self._buffer) < self.amount:
            try:
                self._buffer += self.connection.recv(self.read_buffer)
            except BlockingIOError:
                yield self
        return self._buffer

    def __repr__(self):
        return '%s(file=%s, amount=%d, progress=%d)' % (
            self.__class__.__name__, self.connection, self.amount, len(self._buffer)
        )

とは対照的にAsyncRead__await__真にノンブロッキングI / Oを実行します。データが利用可能な場合、常に読み取ります。使用可能なデータがない場合、データは常に中断されます。つまり、イベントループは、有用な作業を行っている間だけブロックされます。

4.2。イベントループのブロック解除

イベントループに関する限り、それほど大きな変化はありません。待機するイベントは、ファイルの場合と同じselectです。

# old
elif isinstance(command, AsyncRead):
    waiting_read[command.file] = coroutine
# new
elif isinstance(command, AsyncRead):
    waiting_read[command.file] = coroutine
elif isinstance(command, AsyncRecv):
    waiting_read[command.connection] = coroutine

この時点で、AsyncReadAsyncRecvが同じ種類のイベントであることは明らかです。それらを簡単にリファクタリングして、交換可能なI / Oコンポーネントを持つ1つのイベントにすることができます。実際、イベントループ、コルーチン、およびイベント、スケジューラ、任意の中間コード、および実際のI / Oを明確に分離します。

4.3。非ブロッキングI / Oの醜い側面

原則として、この時点ですべきことはreadrecvforのロジックを複製することですAsyncRecv。しかし、これは今よりずっと醜いです-関数がカーネル内でブロックされたときに初期のリターンを処理する必要がありますが、制御はあなたに譲ります。たとえば、ファイルを開くよりも接続を開くほうがはるかに長くなります。

# file
file = open(path, 'rb')
# non-blocking socket
connection = socket.socket()
connection.setblocking(False)
# open without blocking - retry on failure
try:
    connection.connect((url, port))
except BlockingIOError:
    pass

要するに、残っているのは数十行の例外処理です。イベントとイベントループはこの時点ですでに機能しています。

id background round 1
read localhost:25000 at 1530783569
read /dev/urandom at 1530783569
done localhost:25000 at 1530783569 got 32768 B
id background round 2
id background round 3
id background round 4
done /dev/urandom at 1530783569 got 4096 B
id background round 5

補遺

githubのサンプルコード


yield selfAsyncSleepで使用するとTask got back yieldエラーが発生しますが、なぜですか?私はasyncio.Futuresのコードがそれを使用していることを知っています。ベアイールドを使用しても問題ありません。
Ron Serruya

1
イベントループは通常、独自のイベントのみを予期します。通常、ライブラリ間でイベントとイベントループを混在させることはできません。ここに示されているイベントは、示されているイベントループでのみ機能します。具体的には、asyncioはイベントループのシグナルとしてNone(つまり、ベアイールド)のみを使用します。イベントは、イベントループオブジェクトと直接対話して、ウェイクアップを登録します。
MisterMiyagi

12

あなたのcorodesugaringは概念的には正しいですが、少し不完全です。

await無条件に一時停止するのではなく、ブロッキング呼び出しが発生した場合のみ。通話がブロックされていることはどのようにしてわかりますか?これは、待機しているコードによって決まります。たとえば、ソケット読み取りの待機可能な実装は、次のように設計できます。

def read(sock, n):
    # sock must be in non-blocking mode
    try:
        return sock.recv(n)
    except EWOULDBLOCK:
        event_loop.add_reader(sock.fileno, current_task())
        return SUSPEND

実際のasyncioでは、同等のコードFutureがマジック値を返す代わりにaの状態を変更しますが、概念は同じです。ジェネレータのようなオブジェクトに適切に適合させると、上記のコードをawait編集できます。

呼び出し側で、コルーチンに次のものが含まれている場合:

data = await read(sock, 1024)

それは以下に近いものに脱糖します:

data = read(sock, 1024)
if data is SUSPEND:
    return SUSPEND
self.pos += 1
self.parts[self.pos](...)

ジェネレーターに精通している人yield fromは、サスペンションを自動的に行うという点で上記を説明する傾向があります。

中断チェーンは、イベントループまで続きます。これは、コルーチンが中断されていることを認識し、それを実行可能セットから削除し、実行可能なコルーチンがある場合はそれを実行します。コルーチンが実行可能でない場合、ループselect()は、コルーチンが対象とするファイル記述子がIOの準備ができるまで待機します。(イベントループは、ファイル記述子からコルーチンへのマッピングを維持します。)

上記の例でselect()は、sock読み取り可能なイベントループがcoro通知されると、実行可能セットに再度追加されるため、中断した時点から継続されます。

言い換えると:

  1. デフォルトでは、すべてが同じスレッドで発生します。

  2. イベントループは、コルーチンをスケジュールし、コルーチンが待機していたもの(通常は通常ブロックされるIO呼び出し、またはタイムアウト)の準備ができたときにそれらを起動する役割を果たします。

コルーチンを駆動するイベントループの洞察については、ライブオーディエンスの前でイベントループを最初からコーディングすることをデモンストレーションするDave Beazleyによるこの講演をお勧めします。


ありがとう、これは私が求めているものに近いですが、これはなぜasync.wait_for()それが想定されていることをしないのか説明していません...なぜイベントループにコールバックを追加してそれを伝えるのがとても大きな問題なのですか?追加したコールバックを含め、必要なコールバックをいくつでも処理するには?私の不満がasyncio原因根底にある概念は非常に簡単で、かつ、例えば、Emacs Lispには流行語を使用せずに、年齢のための実装を持っていた...(つまり、という事実に一部であるcreate-async-processaccept-process-output-これは必要とされているすべてのです... (続き)
wvxvw

10
@wvxvw最後の段落に6つの質問しか含まれていないことを考えると、あなたが投稿した質問に答えるためにできる限りのことをしました。そして私たちが続けます-それはそれwait_for が想定されていることを実行しないわけではありません(実際、それはあなたが待つべきコルーチンです)、それはあなたの期待がシステムが設計され実装されたものと一致しないことです。イベントループが別のスレッドで実行されている場合、問題はasyncioと一致する可能性がありますが、ユースケースの詳細はわかりません。正直なところ、あなたの態度はあなたを助けることをそれほど楽しくしません。
user4815162342 2018

5
@wvxvw My frustration with asyncio is in part due to the fact that the underlying concept is very simple, and, for example, Emacs Lisp had implementation for ages, without using buzzwords...-Pythonの流行語なしでこの単純な概念を実装することを妨げるものは何もありません:)なぜこの醜いasyncioを使用するのですか?独自にゼロから実装します。たとえば、async.wait_for()本来の機能を実行する独自の関数を作成することから始めることができます。
ミハイルゲラシモ

1
@MikhailGerasimovあなたはそれを修辞的な質問だと思っているようです。しかし、私はあなたのために謎を解き放ちたいのです。言語は他の人と話すように設計されています。彼らが話す言語を他人に選ぶことはできません。たとえ彼らが話す言語がごみであると私が信じているとしても、私ができる最善のことは彼らにそのように納得させることです。言い換えれば、私が自由に選択できれば、Pythonを選択することは決してありませんasyncio。しかし、原則として、それは私の決断ではありません。en.wikipedia.org/wiki/Ultimatum_gameを介してゴミの言語を使用することを強制されています。
wvxvw

4

すべては、asyncioが対処している2つの主要な課題に要約されます。

  • 1つのスレッドで複数のI / Oを実行するにはどうすればよいですか?
  • 協調マルチタスクを実装する方法は?

最初のポイントに対する答えは長い間存在しており、選択ループと呼ばれています。Pythonでは、セレクターモジュールに実装されています

2番目の質問は、コルーチンの概念、つまり実行を停止して後で復元できる関数に関連しています。Pythonでは、コルーチンはジェネレーターyield fromステートメントを使用して実装されます。これが、非同期/待機構文の背後に隠れているものです。

この回答のその他のリソース。


編集:ゴルーチンに関するコメントへの対処:

asyncioのゴルーチンに最も近いものは、実際にはコルーチンではなくタスクです(ドキュメントの違いを参照してください)。Pythonでは、コルーチン(またはジェネレータ)はイベントループまたはI / Oの概念について何も知りません。これは単にyield、現在の状態を維持したまま使用を停止できる機能であるため、後で復元することができます。yield from構文は、透明な方法でそれらを連鎖することができます。

現在、asyncioタスク内では、チェーンの最下部にあるコルーチンが常に未来を生み出しています。この未来は、イベントループにバブルアップし、内部の機械に統合されます。フューチャーが他の内部コールバックによって行われるように設定されている場合、イベントループはフューチャーをコルーチンチェーンに戻すことによってタスクを復元できます。


編集:あなたの投稿のいくつかの質問に対処します:

このシナリオでは、実際にI / Oはどのように行われますか?別のスレッドで?インタプリタ全体が一時停止され、I / Oはインタプリタの外部で行われますか?

いいえ、スレッドでは何も起こりません。I / Oは常にイベントループによって管理され、ほとんどがファイル記述子を介して行われます。ただし、これらのファイル記述子の登録は、通常、高レベルのコルーチンによって隠されているため、ダーティーな作業が発生します。

I / Oとはどういう意味ですか?私のpythonプロシージャがC open()プロシージャを呼び出し、それが割り込みをカーネルに送信して制御を放棄した場合、カーネルコードが実際のI / Oそしてそれが最初に割り込みを送ったPythonプロシージャを起こすまで?原則としてPythonインタープリターはどのようにしてこれが起こっているのかを知ることができますか?

I / Oは、ブロッキング呼び出しです。asyncioでは、すべてのI / O操作はイベントループを経由する必要があります。これは、既に述べたように、イベントループには、同期コードでブロッキング呼び出しが実行されていることを認識する方法がないためです。つまりopen、コルーチンのコンテキスト内で同期を使用することは想定されていません。代わりに、非同期バージョンのを提供するaiofilesなどの専用ライブラリを使用してくださいopen


コルーチンが使用yield fromして実装されていると言っても、実際には何も言われません。yield fromは単なる構文構造であり、コンピューターが実行できる基本的なビルディングブロックではありません。同様に、選択ループ。はい、Goのコルーチンも選択ループを使用していますが、私がしようとしたことはGoでは機能しますが、Pythonでは機能しません。それが機能しなかった理由を理解するには、より詳細な回答が必要です。
wvxvw

申し訳ありません...いいえ、そうではありません。「未来」、「タスク」、「透過的な方法」、「降伏」は単なる流行語であり、プログラミングのドメインからのオブジェクトではありません。プログラミングには、変数、手順、構造があります。したがって、「goroutine is a task」は、疑問を投げかける単なる循環文です。結局のところ、asyncio私にとって、何が何をするかの説明は、Python構文がどのように翻訳されたかを示すCコードに要約されます。
wvxvw 2018

あなたの答えが私の質問に答えない理由をさらに説明するには:あなたが提供したすべての情報では、リンクされた質問に投稿したコードからの私の試みがなぜうまくいかなかったのか、私にはわかりません。このコードが機能するような方法でイベントループを記述できると確信しています。実際、イベントループを作成する必要がある場合、これはイベントループを作成する方法です。
wvxvw 2018

7
@wvxvw同意しない。これらは「流行語」ではなく、多くのライブラリに実装されている高レベルの概念です。たとえば、asyncioタスク、geventグリーンレット、ゴルーチンはすべて同じものに対応しています。つまり、単一のスレッド内で同時に実行できる実行ユニットです。また、Pythonジェネレーターの内部の仕組みを理解したいと思わない限り、asyncioを理解するためにCが必要だとはまったく思いません。
ビンセント

@wvxvw 2番目の編集を参照してください。これにより、いくつかの誤解が解消されます。
ビンセント
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.