この質問は私の別の質問によって動機付けられています: cdefで待つ方法
asyncio
に関するウェブ上の記事やブログ投稿は山ほどありますが、それらはすべて非常に表面的なものです。 asyncio
が実際にどのように実装されているか、およびI/Oを非同期にする理由に関する情報は見つかりませんでした。私はソースコードを読み込もうとしていましたが、最高級のCコードではない何千行もあり、その多くは補助オブジェクトを処理しますが、最も重要なのは、Python構文とどのCコードに変換されるか。
Asycnio自身のドキュメントはさらに有用ではありません。それがどのように機能するかについての情報はありませんが、それを使用する方法についてのガイドラインだけがあります。
Goのコルーチンの実装に精通しており、Pythonが同じことをすることを望んでいました。その場合、上記のリンクで投稿したコードは機能していました。それがなかったので、私は今、理由を見つけようとしています。これまでの私の最善の推測は次のとおりです。間違っている箇所を修正してください:
async def foo(): ...
形式のプロシージャ定義は、実際にはcoroutine
を継承するクラスのメソッドとして解釈されます。async def
は実際にawait
ステートメントによって複数のメソッドに分割されます。これらのメソッドが呼び出されるオブジェクトは、これまでの実行の進捗状況を追跡できます。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インタープリターは、この出来事に気付くことができますか?
この質問に答える前に、いくつかの基本用語を理解する必要があります。既に知っている場合は、これらをスキップしてください。
ジェネレータは、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()
を呼び出すと、インタープリターはテストのフレームをロードし、yield
ed値を返します。 next()
を再度呼び出して、フレームをインタプリタスタックに再度ロードし、別の値をyield
ingで続行します。
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
の独自の形式を使用します。 async
とawait
が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__()
メソッドが実装されたオブジェクトであり、その仕事は特定の状態と結果を保持することです。状態は次のいずれかです。
fut.cancel()
を使用してキャンセルされましたfut.set_result()
を使用した結果セット、または fut.set_exception()
を使用した例外セットにより、futureが終了しました結果は、あなたが推測したように、返されるPythonオブジェクトか、発生する可能性のある例外のいずれかです。
別のimportantfuture
オブジェクトの機能は、add_done_callback()
というメソッドが含まれていることです。このメソッドを使用すると、例外が発生したか終了したかにかかわらず、タスクが完了するとすぐに関数を呼び出すことができます。
タスクオブジェクトは特別な先物であり、コルーチンを包み込み、最も内側と最も外側のコルーチンと通信します。コルーチンのawait
sがフューチャーになるたびに、フューチャーはタスクに戻され(yield from
のように)、タスクはそれを受け取ります。
次に、タスクは未来にバインドします。これは、将来的にadd_done_callback()
を呼び出すことにより行われます。今後、キャンセル、例外、または結果としてPythonオブジェクトのいずれかによって未来が実行される場合、タスクのコールバックが呼び出され、存在するまで上昇します。 。
私たちが答えなければならない最後の書き込みの質問は-IOはどのように実装されていますか?
Asyncioの奥深くには、イベントループがあります。タスクのイベントループ。イベントループの仕事は、タスクが準備ができるたびにタスクを呼び出し、そのすべての作業を1台の作業マシンに統合することです。
イベントループのIO部分は、select
と呼ばれる単一の重要な関数に基づいて構築されます。 Selectは、その下のオペレーティングシステムによって実装されるブロッキング機能で、ソケットで受信データまたは送信データを待機できるようにします。データを受信すると、スリープが解除され、データを受信したソケット、または書き込みの準備ができたソケットを返します。
Asyncioを介してソケットを介してデータを送受信しようとすると、実際に下で行われるのは、すぐに読み取りまたは送信できるデータがあるかどうかを最初に確認することです。 .send()
バッファーがいっぱいであるか、.recv()
バッファーが空の場合、ソケットはselect
関数に登録されます(リストのいずれかに追加するだけで、rlist
はrecv
およびwlist
はsend
に、新たに作成されたawait
nameは変数future
を作成します)そのソケットに結び付けられたオブジェクト。
使用可能なすべてのタスクが先物を待っている場合、イベントループはselect
を呼び出して待機します。ソケットの1つに着信データがあるか、send
バッファーが使い果たされると、asyncioはそのソケットに関連付けられた将来のオブジェクトをチェックし、完了に設定します。
今、すべての魔法が起こります。 futureはdoneに設定され、それ以前にadd_done_callback()
で追加されたタスクは元の状態に戻り、コルーチンで.send()
を呼び出し、最も内側のコルーチンを再開し(await
チェーンのため)、新しい受信データを読み取りますこぼれた近くのバッファ。
recv()
の場合のメソッドチェーン:
select.select
待機します。future.set_result()
が呼び出されます。add_done_callback()
で自分自身を追加したタスクは、ウェイクアップされます。.send()
を呼び出します。これは、最も内側のコルーチンに到達し、起動します。要約すると、asyncioは関数を一時停止および再開できるジェネレーター機能を使用します。 yield from
機能を使用して、最も内側のジェネレーターから最も外側のジェネレーターにデータをやり取りできます。 IOが完了するのを待っている間(OSのselect
関数を使用して)、関数の実行を停止するためにこれらすべてを使用します。
そして何よりも素晴らしいのは? 1つの機能が一時停止している間、別の機能が実行され、デリケートなファブリック(asyncio)とインターリーブする場合があります。
async/await
とasyncio
について話すことは同じことではありません。前者は基本的な低レベルの構造(コルーチン)であり、後者はこれらの構造を使用するライブラリです。逆に、単一の究極の答えはありません。
以下は、async/await
およびasyncio
- likeライブラリがどのように機能するかの一般的な説明です。つまり、上に他のトリックがあるかもしれません(...があります)が、自分で作成しない限り、それらは重要ではありません。そのような質問をする必要がないほど十分に知っている場合を除き、違いはごくわずかです。
subroutines(functions、procedure、...)、coroutinesと同様(ジェネレーター、...)は呼び出しスタックと命令ポインターの抽象化です。実行中のコードのスタックがあり、それぞれが特定の命令にあります。
def
とasync def
の違いは、単に明確にするためです。実際の違いは、return
対yield
です。このことから、await
またはyield from
は、スタック全体への個々の呼び出しとは異なります。
サブルーチンは、ローカル変数を保持するための新しいスタックレベルと、最後に到達するための命令の単一のトラバースを表します。次のようなサブルーチンを検討してください。
def subfoo(bar):
qux = 3
return qux * bar
実行すると、つまり
bar
およびqux
のスタックスペースを割り当てます。return
で一度、その値を呼び出しスタックにプッシュします特に、4はサブルーチンが常に同じ状態で開始することを意味します。関数自体に排他的なものはすべて、完了時に失われます。 return
の後に命令がある場合でも、関数を再開できません。
root -\
: \- subfoo --\
:/--<---return --/
|
V
コルーチンはサブルーチンに似ていますが、終了することができますwithoutその状態を破壊します。次のようなコルーチンを検討してください。
def cofoo(bar):
qux = yield bar # yield marks a break point
return qux
実行すると、つまり
bar
およびqux
のスタックスペースを割り当てます。yield
で、その値を呼び出しスタックにプッシュしますただし、スタックと命令ポインターを保存しますyield
を呼び出したら、スタックと命令ポインターを復元し、qux
に引数をプッシュしますreturn
で一度、その値を呼び出しスタックにプッシュします2.1と2.2が追加されていることに注意してください-コルーチンは事前定義されたポイントで一時停止および再開できます。これは、別のサブルーチンの呼び出し中にサブルーチンが中断される方法に似ています。違いは、アクティブなコルーチンがその呼び出しスタックに厳密にバインドされていないことです。代わりに、中断されたコルーチンは、分離された独立したスタックの一部です。
root -\
: \- cofoo --\
:/--<+--yield --/
| :
V :
これは、中断されたコルーチンをスタック間で自由に保存または移動できることを意味します。コルーチンにアクセスできる呼び出しスタックは、それを再開することを決定できます。
これまでのところ、コルーチンはyield
でのみコールスタックを下げます。サブルーチンは、下に行くことができますそして上return
と()
の呼び出しスタック。完全を期すために、コルーチンには呼び出しスタックを上げるメカニズムも必要です。次のようなコルーチンを検討してください。
def wrap():
yield 'before'
yield from cofoo()
yield 'after'
それを実行すると、それはサブルーチンのようにスタックと命令ポインターをまだ割り当てることを意味します。中断した場合でも、それはサブルーチンを保存するようなものです。
ただし、yield from
はbothを実行します。 wrap
andのスタックと命令ポインターを一時停止し、cofoo
を実行します。 wrap
は、cofoo
が完全に終了するまで中断されたままになることに注意してください。 cofoo
が一時停止するか何かが送信されるたびに、cofoo
は呼び出しスタックに直接接続されます。
確立されているように、yield from
を使用すると、2つのスコープを別の中間スコープに接続できます。再帰的に適用される場合、スタックのtopをスタックのbottomに接続できることを意味します。
root -\
: \-> coro_a -yield-from-> coro_b --\
:/ <-+------------------------yield ---/
| :
:\ --+-- coro_a.send----------yield ---\
: coro_b <-/
root
とcoro_b
はお互いを知らないことに注意してください。これにより、コルーチンはコールバックよりもずっときれいになります。コルーチンは、サブルーチンのような1:1の関係に基づいて構築されたままです。コルーチンは、通常の呼び出しポイントまで、既存の実行スタック全体を一時停止および再開します。
特に、root
には、再開する任意の数のコルーチンを含めることができます。ただし、同時に複数を再開することはできません。同じルートのコルーチンは並行ですが、並行ではありません!
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
チェーンを分割するためです。
コルーチン自体には、anotherコルーチンに制御を渡すという概念はありません。コルーチンスタックの一番下にある呼び出し元にのみ制御を渡すことができます。この呼び出し元は、別のコルーチンに切り替えて実行できます。
いくつかのコルーチンのこのルートノードは通常、イベントループです:一時停止の場合、コルーチンはイベント再開したい場所。また、イベントループは、これらのイベントの発生を効率的に待機できます。これにより、次に実行するコルーチン、または再開する前に待機する方法を決定できます。
このような設計は、ループが理解する一連の定義済みイベントがあることを意味します。最後にイベントがawait
edになるまで、いくつかのコルーチンがawait
お互いに。このイベントは、yield
ingコントロールにより、イベントループと直接を通信できます。
loop -\
: \-> coroutine --await--> event --\
:/ <-+----------------------- yield --/
| :
| : # loop waits for event to happen
| :
:\ --+-- send(reply) -------- yield --\
: coroutine <--yield-- event <-/
重要なのは、コルーチンの中断により、イベントループとイベントが直接通信できることです。中間コルーチンスタックは、any実行しているループに関する知識やイベントの動作方法を必要としません。
処理する最も簡単なイベントは、ある時点に到達することです。これは、スレッド化されたコードの基本ブロックでもあります。条件が真になるまで、スレッドは繰り返しsleep
sになります。ただし、通常のsleep
は単独で実行をブロックします-他のコルーチンがブロックされないようにします。代わりに、現在のコルーチンスタックを再開するタイミングをイベントループに通知する必要があります。
イベントとは、単に識別可能な値です。列挙型、型、またはその他の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
キーワードが探すものです。実際には、それは反復子ですが、通常の反復機構では使用できません。
イベントができたので、コルーチンはどのように反応しますか?イベントをsleep
ingすることで、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
を使用すると、結果がyield
sになるまでコルーチンが実行されます。
coroutine = asleep(100)
while True:
print(coroutine.send(None))
time.sleep(0.1)
これにより、2つのAsyncSleep
イベントと、コルーチンが完了するとStopIteration
が得られます。ループ内のtime.sleep
からの遅延のみであることに注意してください!各AsyncSleep
は、現在の時刻からのオフセットのみを格納します。
この時点で、自由に利用できるtwo別個のメカニズムがあります。
AsyncSleep
コルーチン内から生成できるイベントtime.sleep
コルーチンに影響を与えずに待機できます特に、これら2つは直交しています。一方が他方に影響を与えることもトリガーすることもありません。その結果、sleep
の遅延に対応するために、AsyncSleep
に対する独自の戦略を考え出すことができます。
severalコルーチンがある場合、それぞれが起こされたいときを教えてくれます。その後、最初の人が再開を希望するまで待機し、その後、次の人を再開するまで待機します。特に、各ポイントでは、どちらがnextであるかのみを考慮します。
これにより、簡単なスケジューリングが可能になります。
些細な実装では、高度な概念は必要ありません。 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
から戻り値を取得して、コルーチンに割り当てることもできます。ただし、基本的な原則は変わりません。
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秒で作業を実行します。各コルーチンは状態を保持し、独立して動作します。
sleep
をサポートするイベントループは、pollingに適しています。ただし、ファイルハンドルでのI/Oの待機はより効率的に実行できます。オペレーティングシステムはI/Oを実装するため、どのハンドルの準備ができているかがわかります。理想的には、イベントループは明示的な「I/Oの準備完了」イベントをサポートする必要があります。
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
には開いているファイルが含まれます。
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の結果を取得します。
イベントループの基礎は、以前に定義された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):
...
上記は少し単純化されました。常に読むことができる場合は、寝ているコルーチンを飢えさせないように切り替える必要があります。読むものも待つものもないことを処理する必要があります。ただし、最終結果は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
AsyncSleep
、AsyncRead
、および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
ファイルのI/Oは概念を理解しますが、asyncio
:select
呼び出し 常にファイルを返す 、および両方のopen
およびread
は、 無期限にブロック を指定できます。これは、イベントループのすべてのコルーチンをブロックします-これは悪いことです。 aiofiles
などのライブラリは、スレッドと同期を使用して、ファイルの非ブロッキングI/Oおよびイベントを偽装します。
ただし、ソケットはノンブロッキング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一時停止します。つまり、有用な作業を実行している間のみイベントループがブロックされます。
イベントループに関する限り、大きな変更はありません。リッスンするイベントは、ファイルの場合と同じです-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
この時点で、AsyncRead
とAsyncRecv
は同じ種類のイベントであることは明らかです。簡単にリファクタリングして、交換可能なI/Oコンポーネントを持つoneイベントにできます。実際には、イベントループ、コルーチン、およびイベント クリーンに分離 スケジューラ、任意の中間コード、および実際のI/O。
原則として、この時点で行うべきことは、read
のロジックをrecv
の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
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
の状態を変更しますが、概念は同じです。ジェネレータのようなオブジェクトに適切に適応する場合、上記のコードはawait
edにできます。
呼び出し側で、コルーチンに以下が含まれる場合:
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
を実行可能セットに再度追加するため、一時停止のポイントから継続されます。
言い換えると:
すべてはデフォルトで同じスレッドで発生します。
イベントループは、コルーチンをスケジュールし、待機しているもの(通常、通常ブロックする、またはタイムアウトするIO呼び出し)が準備できたときに起動します。
コルーチン駆動イベントループに関する洞察については、Dave Beazleyによる this talk をお勧めします。DaveBeazleyは、ライブオーディエンスの前でイベントループをゼロからコーディングする方法を示しています。
それはすべて、asyncioが取り組んでいる2つの主な課題に要約されます。
最初のポイントに対する答えは長い間存在しており、 select loop と呼ばれています。 Pythonでは、 selectorsモジュール で実装されています。
2番目の質問は、 コルーチン の概念に関連しています。つまり、実行を停止して後で復元できる関数です。 Pythonでは、コルーチンは generators と yield 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
の非同期バージョンを提供します。