話すこと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 from2つのスコープを別の中間スコープに接続できます。再帰的に適用すると、スタックの上部をスタックの下部に接続できます。
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、recvforのロジックを複製することです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/...