話すことasync/await
とasyncio
同じことではありません。最初のものは基本的な低レベルの構成要素(コルーチン)であり、後者はこれらの構成要素を使用するライブラリです。逆に、単一の最終的な答えはありません。
以下は、async/await
および- asyncio
ライクなライブラリの動作の概要です。つまり、上に他のトリックがあるかもしれません(ある...)が、自分で構築しない限り、重要ではありません。あなたがそのような質問をする必要がないほど十分に知っているのでない限り、違いは無視できるはずです。
1.コルーチン対ナッツ殻のサブルーチン
同じようにサブルーチン(関数、プロシージャ、...)、コルーチンコード片を実行するスタックがあり、それぞれが特定の命令である:(ジェネレータは、...)コールスタックと命令ポインタの抽象化です。
def
対の区別async def
は、単に明確にするためです。実際の違いはreturn
とyield
です。これから、await
またはyield from
個々の呼び出しとスタック全体の違いを理解してください。
1.1。サブルーチン
サブルーチンは、ローカル変数を保持するための新しいスタックレベルと、最後に到達するためのその命令の単一のトラバーサルを表します。次のようなサブルーチンについて考えてみます。
def subfoo(bar):
qux = 3
return qux * bar
実行すると、それは
bar
およびにスタックスペースを割り当てるqux
- 最初のステートメントを再帰的に実行し、次のステートメントにジャンプします
- 一度、
return
その値を呼び出しスタックにプッシュします
- スタック(1.)と命令ポインタ(2.)をクリアします。
特に、4。は、サブルーチンが常に同じ状態で開始されることを意味します。関数自体に排他的なものはすべて、完了すると失われます。の後に指示があっても、機能を再開することはできませんreturn
。
root -\
: \- subfoo --\
:/--<---return --/
|
V
1.2。永続的なサブルーチンとしてのコルーチン
コルーチンはサブルーチンのようなものですが、その状態を破壊することなく終了できます。このようなコルーチンを考えてみましょう:
def cofoo(bar):
qux = yield bar # yield marks a break point
return qux
実行すると、それは
bar
およびにスタックスペースを割り当てるqux
- 最初のステートメントを再帰的に実行し、次のステートメントにジャンプします
- 一度、
yield
その値を呼び出しスタックにプッシュしますが、スタックと命令ポインタを保存します
- を呼び出すと
yield
、スタックと命令ポインタを復元し、引数をqux
- 一度、
return
その値を呼び出しスタックにプッシュします
- スタック(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 from
2つのスコープを別の中間スコープに接続できます。再帰的に適用すると、スタックの上部をスタックの下部に接続できます。
root -\
: \-> coro_a -yield-from-> coro_b --\
:/ <-+------------------------yield ---/
| :
:\ --+-- coro_a.send----------yield ---\
: coro_b <-/
お互いに知らないことに注意しroot
てcoro_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
あなたは破るためのステートメントが必要とされてyield from/await
裸でチェーンfor
やwith
文を。
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。イベント待ち
イベントができたので、コルーチンはそれにどのように反応しますか?私たちは、同等のものを表現することができるはずsleep
で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
結果が出るまでコルーチンが実行されます。
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から繰り返します。
簡単な実装では、高度な概念は必要ありません。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
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
4.非ブロッキングI / O
I / Oファイルには全体のコンセプトを取得しますが、それは次のようにライブラリのために本当に適していませんasyncio
:select
呼び出しは常にファイルに対して返し、両方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を実行します。データが利用可能な場合、常に読み取ります。使用可能なデータがない場合、データは常に中断されます。つまり、イベントループは、有用な作業を行っている間だけブロックされます。
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
この時点で、AsyncRead
とAsyncRecv
が同じ種類のイベントであることは明らかです。それらを簡単にリファクタリングして、交換可能なI / Oコンポーネントを持つ1つのイベントにすることができます。実際、イベントループ、コルーチン、およびイベントは、スケジューラ、任意の中間コード、および実際のI / Oを明確に分離します。
4.3。非ブロッキングI / Oの醜い側面
原則として、この時点ですべきことはread
、recv
forのロジックを複製することです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のサンプルコード
BaseEventLoop
実装されていますgithub.com/python/cpython/blob/...