web-dev-qa-db-ja.com

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

この質問は私の別の質問によって動機付けられています: cdefで待つ方法

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インタープリターは、この出来事に気付くことができますか?

53
wvxvw

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()を再度呼び出して、フレームをインタプリタスタックに再度ロードし、別の値をyieldingで続行します。

next()が3回目に呼び出されると、ジェネレーターは終了し、 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():
...     print((yield 2))
...     return 3
...
>>> def outer():
...     yield 1
...     val = yield from inner()
...     print(val)
...     yield 4
...
>>> gen = outer()
>>> next(gen)
1
>>> next(gen)
2
>>> gen.send("abc")
abc
3
4

すべてを一緒に入れて

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

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

async def inner():
    return 1

async def outer():
    await inner()

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

ニース シーケンス図Pythonドキュメント の中にあり、チェックアウトする必要があります。

Asyncioには、コルーチン関数とは別に、2つの重要なオブジェクトがあります:tasksおよびfutures

先物

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

  1. PENDING-futureには結果または例外セットはありません。
  2. キャンセル-futureはfut.cancel()を使用してキャンセルされました
  3. FINISHED- fut.set_result() を使用した結果セット、または fut.set_exception() を使用した例外セットにより、futureが終了しました

結果は、あなたが推測したように、返されるPythonオブジェクトか、発生する可能性のある例外のいずれかです。

別のimportantfutureオブジェクトの機能は、add_done_callback()というメソッドが含まれていることです。このメソッドを使用すると、例外が発生したか終了したかにかかわらず、タスクが完了するとすぐに関数を呼び出すことができます。

タスク

タスクオブジェクトは特別な先物であり、コルーチンを包み込み、最も内側と最も外側のコルーチンと通信します。コルーチンのawaitsがフューチャーになるたびに、フューチャーはタスクに戻され(yield fromのように)、タスクはそれを受け取ります。

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

非同期

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

Asyncioの奥深くには、イベントループがあります。タスクのイベントループ。イベントループの仕事は、タスクが準備ができるたびにタスクを呼び出し、そのすべての作業を1台の作業マシンに統合することです。

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

Asyncioを介してソケットを介してデータを送受信しようとすると、実際に下で行われるのは、すぐに読み取りまたは送信できるデータがあるかどうかを最初に確認することです。 .send()バッファーがいっぱいであるか、.recv()バッファーが空の場合、ソケットはselect関数に登録されます(リストのいずれかに追加するだけで、rlistrecvおよびwlistsendに、新たに作成されたawaitnameは変数futureを作成します)そのソケットに結び付けられたオブジェクト。

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

今、すべての魔法が起こります。 futureはdoneに設定され、それ以前に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関数を使用して)、関数の実行を停止するためにこれらすべてを使用します。

そして何よりも素晴らしいのは? 1つの機能が一時停止している間、別の機能が実行され、デリケートなファブリック(asyncio)とインターリーブする場合があります。

95
Bharel

async/awaitasyncioについて話すことは同じことではありません。前者は基本的な低レベルの構造(コルーチン)であり、後者はこれらの構造を使用するライブラリです。逆に、単一の究極の答えはありません。

以下は、async/awaitおよびasyncio- likeライブラリがどのように機能するかの一般的な説明です。つまり、上に他のトリックがあるかもしれません(...があります)が、自分で作成しない限り、それらは重要ではありません。そのような質問をする必要がないほど十分に知っている場合を除き、違いはごくわずかです。

1. nut shellのコルーチンとサブルーチン

subroutines(functions、procedure、...)、coroutinesと同様(ジェネレーター、...)は呼び出しスタックと命令ポインターの抽象化です。実行中のコードのスタックがあり、それぞれが特定の命令にあります。

defasync 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。永続サブルーチンとしてのコルーチン

コルーチンはサブルーチンに似ていますが、終了することができますwithoutその状態を破壊します。次のようなコルーチンを検討してください。

 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 frombothを実行します。 wrapandのスタックと命令ポインターを一時停止し、cofooを実行します。 wrapは、cofooが完全に終了するまで中断されたままになることに注意してください。 cofooが一時停止するか何かが送信されるたびに、cofooは呼び出しスタックに直接接続されます。

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

確立されているように、yield fromを使用すると、2つのスコープを別の中間スコープに接続できます。再帰的に適用される場合、スタックのtopをスタックのbottomに接続できることを意味します。

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

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

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

1.5。 Pythonのasyncおよびawait

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

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ステートメントが必要なのは、裸のforおよびwithステートメントでyield from/awaitチェーンを分割するためです。

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

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

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

このような設計は、ループが理解する一連の定義済みイベントがあることを意味します。最後にイベントがawaitedになるまで、いくつかのコルーチンがawaitお互いに。このイベントは、yieldingコントロールにより、イベントループと直接を通信できます。

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

重要なのは、コルーチンの中断により、イベントループとイベントが直接通信できることです。中間コルーチンスタックは、any実行しているループに関する知識やイベントの動作方法を必要としません。

2.1.1。時間内のイベント

処理する最も簡単なイベントは、ある時点に到達することです。これは、スレッド化されたコードの基本ブロックでもあります。条件が真になるまで、スレッドは繰り返しsleepsになります。ただし、通常の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)

このクラスのみstoresイベント-実際にどのように処理するかを指定しません。

唯一の特別な機能は__await__です-awaitキーワードが探すものです。実際には、それは反復子ですが、通常の反復機構では使用できません。

2.2.1。イベントを待っています

イベントができたので、コルーチンはどのように反応しますか?イベントをsleepingすることで、awaitに相当するものを表現できるはずです。何が起こっているのかをよりよく見るために、半分の時間で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を使用すると、結果がyieldsになるまでコルーチンが実行されます。

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

これにより、2つのAsyncSleepイベントと、コルーチンが完了するとStopIterationが得られます。ループ内のtime.sleepからの遅延のみであることに注意してください!各AsyncSleepは、現在の時刻からのオフセットのみを格納します。

2.2.2。イベント+スリープ

この時点で、自由に利用できるtwo別個のメカニズムがあります。

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

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

2.3。素朴なイベントループ

severalコルーチンがある場合、それぞれが起こされたいときを教えてくれます。その後、最初の人が再開を希望するまで待機し、その後、次の人を再開するまで待機します。特に、各ポイントでは、どちらがnextであるかのみを考慮します。

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

  1. 希望するウェイクアップ時間でコルーチンをソートします
  2. 目覚めたい最初のものを選んでください
  3. この時点まで待つ
  4. このコルーチンを実行する
  5. 1から繰り返します。

些細な実装では、高度な概念は必要ありません。 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をサポートするイベントループは、pollingに適しています。ただし、ファイルハンドルでのI/Oの待機はより効率的に実行できます。オペレーティングシステムはI/Oを実装するため、どのハンドルの準備ができているかがわかります。理想的には、イベントループは明示的な「I/Oの準備完了」イベントをサポートする必要があります。

3.1。 select呼び出し

Pythonには、読み取りI/OハンドルについてOSにクエリするためのインターフェイスが既にあります。読み取りまたは書き込みハンドルを使用して呼び出されると、読み取りまたは書き込みハンドルreadyを返します。

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。協調入出力

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呼び出し 常にファイルを返す 、および両方のopenおよびreadは、 無期限にブロック を指定できます。これは、イベントループのすべてのコルーチンをブロックします-これは悪いことです。 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を実行します。データが利用可能になると、alwaysが読み込まれます。利用可能なデータがない場合、always一時停止します。つまり、有用な作業を実行している間のみイベントループがブロックされます。

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コンポーネントを持つoneイベントにできます。実際には、イベントループ、コルーチン、およびイベント クリーンに分離 スケジューラ、任意の中間コード、および実際のI/O。

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

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

# 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のサンプルコード

41
MisterMiyagi

coroの脱糖は概念的には正しいですが、少し不完全です。

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の状態を変更しますが、概念は同じです。ジェネレータのようなオブジェクトに適切に適応する場合、上記のコードはawaitedにできます。

呼び出し側で、コルーチンに以下が含まれる場合:

data = await read(sock, 1024)

以下に近いものに脱糖します。

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

ジェネレーターに精通している人々は、上記のことをyield fromの観点から説明する傾向があります。

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

上記の例では、select()sockが読み取り可能であることをイベントループに伝えると、coroを実行可能セットに再度追加するため、一時停止のポイントから継続されます。

言い換えると:

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

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

コルーチン駆動イベントループに関する洞察については、Dave Beazleyによる this talk をお勧めします。DaveBeazleyは、ライブオーディエンスの前でイベントループをゼロからコーディングする方法を示しています。

7
user4815162342

それはすべて、asyncioが取り組んでいる2つの主な課題に要約されます。

  • 単一のスレッドで複数のI/Oを実行する方法は?
  • 協調マルチタスクを実装する方法は?

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

2番目の質問は、 コルーチン の概念に関連しています。つまり、実行を停止して後で復元できる関数です。 Pythonでは、コルーチンは generatorsyield from ステートメントを使用して実装されます。それが async/await構文 の背後に隠れているものです。

この他のリソース answer


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

Asyncioのゴルーチンに最も近いものは、実際にはコルーチンではなくタスクです( documentation の違いを参照)。 Pythonでは、コルーチン(またはジェネレーター)はイベントループまたはI/Oの概念について何も知りません。これは、現在の状態を維持したままyieldを使用して実行を停止できる関数なので、後で復元できます。 yield from構文を使用すると、それらを透過的にチェーンできます。

現在、asyncioタスク内で、チェーンの一番下のコルーチンは常に future を生成します。この未来は、イベントループにバブルアップし、内部の機械に統合されます。 futureが他の内部コールバックによってdoneに設定されている場合、イベントループはfutureをコルーチンチェーンに送り返すことでタスクを復元できます。


EDIT:投稿内のいくつかの質問に対処する:

このシナリオでI/Oは実際にどのように発生しますか?別のスレッドで?通訳者全体が中断され、I/Oは通訳者の外部で発生しますか?

いいえ、スレッドでは何も起こりません。 I/Oは常にファイルディスクリプタを介して、イベントループによって常に管理されます。ただし、これらのファイル記述子の登録は通常、高レベルのコルーチンによって隠されており、面倒な作業を行っています。

I/Oの正確な意味は何ですか? pythonプロシージャがC open()プロシージャを呼び出し、カーネルに割り込みを送信して制御を放棄した場合、Pythonインタープリターはこれをどのように認識し、実行を継続できますか他のコード、カーネルコードは実際のI/Oを行い、割り込みを最初に送信したPythonプロシージャを起動するまで?原則としてPythonインタープリターは、この出来事に気付くことができますか?

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

3
Vincent