ホームページ >バックエンド開発 >Python チュートリアル >Python は再開された非同期 IO をサポートします

Python は再開された非同期 IO をサポートします

高洛峰
高洛峰オリジナル
2016-10-18 11:50:131172ブラウズ

概要


これは、Python3.3 から始まる Python3 非同期 I/O の提案です。 PEP 3153 に含まれていない特定の提案を検討します。 この提案には、プラグイン可能なイベント ループ API、Twisted に似たトランスポートおよびプロトコルの抽象化、および (PEP 380) の高レベルの収量ベースのスケジューラが含まれています。作品内の参照実装であり、そのコードは Tulip という名前です (Tulip のリポジトリへのリンクは記事の最後にある参照セクションにあります)。

はじめに


イベント ループは、相互運用性の高い場所でよく使用されます。 Twisted、Tornado、ZeroMQ (Python 3.3 ベース) などのフレームワークの場合、フレームワークのニーズに応じて軽量のラッパーまたはプロキシを介してデフォルトのイベント ループ実装を適応させたり、独自のイベント ループ実装を使用してデフォルトを置き換えたりすることが簡単である必要があります。実装。 (Twisted などの一部のフレームワークには、複数のイベント ループ実装があります。これらの実装はすべて統一インターフェイスを備えているため、これは問題になりません。)

2 つの異なるサードパーティ フレームワークがイベント ループと対話することも可能です。デフォルトのイベント ループ実装を共有する (それぞれが独自のアダプターを使用する) か、いずれかのフレームワークのイベント ループ実装を共有する。後者では、2 つの異なるレベルの適応が存在する可能性があります (フレームワーク A のイベント ループから標準イベント ループ インターフェイスへ、次に標準からフレームワーク B のイベント ループへ)。使用されるイベント ループの実装は、メイン プログラムの制御下にある必要があります (ただし、イベント ループが選択できるデフォルトの戦略が提供されています)。

したがって、2 つの別個の API が定義されます:

現在のイベントのループ オブジェクトの取得と設定;

イベント ループ オブジェクトとその最小保証を識別するインターフェイス

イベント ループ実装は追加のメソッドと保証を提供する場合があります。

イベント ループ インターフェイスは出力に依存せず、代わりにコールバック、追加のインターフェイス (トランスポート プロトコルとプロトコル)、およびフューチャー (?) の組み合わせを使用します。後者は PEP3148 で定義されたインターフェイスに似ていますが、実装が異なり、スレッドにバインドされていません。特に、wait() メソッドがないため、ユーザーはコールバックを使用します。

コールバック関数を使いたくない人 (私も含めて) のために、(Python) は PEP 380 の yield from 式を使用するコルーチンとして非同期 I/O コードを記述するためのスケジューラーを提供します。このスケジューラはプラグ可能ではありません。プラグ可能性はイベント ループ レベルで存在し、スケジューラは標準に準拠したイベント ループ実装で動作する必要があります。

コルーチンやその他の非同期フレームワークを使用するコードとの相互運用性を確保するために、スケジューラーには Future のように動作する Task クラスがあります。イベント ループ レベルで対話するフレームワークは、Future にコールバック関数を追加し、Future が完了するのを待つことができます。同様に、スケジューラは、コールバック関数が呼び出されるまでコルーチンを一時停止する操作を提供します。

イベント ループ インターフェイスを介してスレッド間の対話に制約を提供します。(Python では) イベント ループ互換の Future を返すことができる関数をエグゼキューターに送信できる API があります (PEP 3148 を参照)。

目的なし


スタックレス Python や greenlets/gevent などのシステムの相互運用性は、このチュートリアルの目的ではありません。

仕様


依存関係


Python3.3が必要です。 Python 3.3 の範囲を超えて、新しい言語や標準ライブラリは必要ありません。サードパーティのモジュールやパッケージは必要ありません。

モジュール名前空間


ここでの仕様は、新しいトップレベルパッケージに配置されます。さまざまなコンポーネントがこのパッケージのさまざまなサブモジュールに配置されます。パッケージは、よく使用される API をそれぞれのサブモジュールからインポートし、パッケージ プロパティとして利用できるようにします (電子メール パッケージの実行方法と同様)。

最上位パッケージの名前はまだ指定されていません。リファレンス実装の名前は「tulip」ですが、この実装が標準ライブラリ (できれば Python 3.4) に追加されると、この名前はもっと厄介なものに変更される可能性があります。

迷惑な名前を選択する前に、このチュートリアルでは最上位パッケージの名前として「tulip」を使用します。特定のモジュール名のないクラスと関数は、トップレベルのパッケージを通じてアクセスされると想定されます。

イベント ループ戦略: イベント ループの取得と設定


現在のイベント ループを取得するには、get_event_loop() を使用できます。この関数は、以下に定義されている EventLoop クラスのインスタンス、または同等のオブジェクトを返します。 get_event_loop() は、現在のスレッドに基づいて異なるオブジェクト、または他のコンテキスト概念に基づいて異なるオブジェクトを返す場合があります。

現在のイベント ループを設定するには、set_event_loop(event_loop) を使用できます。ここで、event_loop は EventLoop クラスのインスタンスまたは同等のインスタンス オブジェクトです。ここでは get_event_loop() と同じコンテキストの概念が使用されています。

3 番目の戦略関数 new_event_loop() もあります。これは、単体テストやその他の特殊な状況に役立ちます。戦略のデフォルト ルールに基づいて、新しい EventLoop インスタンスを作成して返します。これを現在のイベント ループにするには、set_event_loop() を呼び出す必要があります。

上記の 3 つの関数の動作方法 (コンテキストの概念を含む) を変更するには、set_event_loop_policy(policy) を呼び出すことができます。ここで、パラメーター ポリシーはイベント ループ ポリシー オブジェクトです。このポリシー オブジェクトは、上記で説明した関数 (get_event_loop()、set_event_loop(event_loop)、および new_event_loop()) と同様の関数を含む任意のオブジェクトにすることができます。デフォルトのイベント ループ ポリシーは、DefaultEventLoopPolicy クラスのインスタンスです。現在のイベント ループ ポリシー オブジェクトは、get_event_loop_policy() を呼び出すことで取得できます。

イベント ループ戦略では、イベント ループが 1 つだけ存在できるようにする必要はありません。デフォルトのイベント ループ ポリシーはこれを強制しませんが、スレッドごとにイベント ループが 1 つだけ存在できることを強制します。

イベントループインターフェース


時間について: Python では、すべてのタイムアウト、間隔、遅延は秒単位で計算され、整数または浮動小数点型になります。クロックの精度は実装に依存します。デフォルトは time.monotonic() です。

コールバックとハンドラーについて: 関数がコールバック関数と任意の数の変数をパラメーターとして受け入れる場合、コールバック関数をハンドラー関数オブジェクト (Handler) に置き換えることもできます。この場合、それらのパラメータを渡す必要はありません。このハンドラー関数オブジェクトは、遅延リターン (fromcall_later()) ではなく、即時にリターンする関数 (fromcall_soon()) である必要があります。ハンドラーがキャンセルされている場合、この呼び出しは効果がありません。

標準準拠のイベント ループ オブジェクトには、

run() メソッドがあります。 他に何もすることがなくなるまでイベント ループを実行します。具体的な意味は次のとおりです:

通話のキャンセルを除き、call_later()、call_repeatly()、call_soon()、orcall_soon_threadsafe() を通じてスケジュールされた通話はこれ以上ありません。

登録されたファイル記述子はもうありません。 ファイル記述子は、閉じられると登録者によって登録解除されます。

注: run() は、終了条件に遭遇するか stop() を呼び出すまでブロックされます。

注: call_repeatly() を使用して呼び出しを実行する場合、stop() を呼び出す前に run() は終了しません。

詳しく説明する必要があります: 同様のことを実際にいくつ行う必要があるでしょうか?

run_forever()。イベント ループは、stop() が呼び出されるまで実行されます。

run_until_complete(将来、タイムアウト = なし)。イベント ループは、Future が完了するまで実行されます。タイムアウト値が指定されている場合は、タイムアウト時間の間待機します。 Future が完了すると、その結果が返されるか、例外がスローされます。Future がタイムアウト前に完了するか、stop() が呼び出される場合は、TimeoutError がスローされます (ただし、イベント ループ中に Future はキャンセルされません)。はすでに実行されているため、このメソッドを呼び出すことはできません。

注: この API はテストまたは同様の作業によく使用されます。 これは、Future を待つ式やその他のメソッドからの yield の future の代替として使用しないでください。 (例: 完了コールバックの登録)。

run_once(タイムアウト=なし)。イベントのセグメントに対してイベント ループを実行します。 タイムアウト値が指定されている場合、I/O ポーリングは一定期間ブロックされます。それ以外の場合、I/O ポーリングは時間制限がありません。

注: 正確に言えば、ここで行われる作業量は特定の実装によって異なります。 制約の 1 つは、call_soon() を使用して自分自身を直接スケジュールすると、失敗が発生し、それでも run_once() が返されるということです。

やめて()。イベント ループをできるだけ早く停止します。その後、run() を使用してループ (またはその変形) を再開できます。

注: 特定の実装に基づいて、停止するブロックが複数あります。 stop() の前に実行されていたすべての直接コールバック関数は引き続き実行されている必要がありますが、stop() が呼び出された後にスケジュールされたコールバック関数 (または遅延されたコールバック関数) は実行されません。

閉じる()。イベント ループを閉じ、epoll() や kqueue() で使用されるファイル記述子など、イベント ループが保持していたリソースを解放します。イベント ループの実行中にこのメソッドを呼び出さないでください。複数回呼び出すことができます。

call_later(遅延、コールバック、*引数)。 callback(*args) は、キャンセルされない限り、呼び出されるまで約遅延秒の遅延でスケジュールします。コールバック関数を表す Handler オブジェクトを返します。 Handler オブジェクトの cancel() メソッドは、コールバック関数をキャンセルするためによく使用されます。

call_repeatly(間隔、コールバック、**引数)。 call_later() と似ていますが、コールバック関数は、返されたハンドラーがキャンセルされるまで、interval 秒ごとに繰り返し呼び出されます。最初の呼び出しは間隔秒以内に行われます。

call_soon(コールバック, *args)。 call_later(0, callback, *args) に似ています。

call_soon_threadsafe(callback, *args)。 call_soon(callback, *args) と似ていますが、イベント ループが IO を待ってブロックされ、別のスレッドで呼び出される場合、イベント ループのブロックはキャンセルされます。これは、別のスレッドから安全に呼び出すことができる唯一のメソッドです。 (スレッドセーフな方法で遅延のあるコールバック関数をスケジュールするには、ev.call_soon_threadsafe(ev.call_later,when,callback,*args) を使用できます。) ただし、シグナル ハンドラーから呼び出すのは安全ではありません (シグナル ハンドラーから呼び出すのは安全ではありません)。ロックも使用可能です)。

add_signal_handler(sig, コールバック, *args)。シグナル「sigis」が受信されるたびに、callback(*args) が呼び出されるようにスケジュールされます。シグナルのコールバック関数をキャンセルするために使用できるハンドラーを返します。 (リターン ハンドラーをキャンセルすると、次のシグナルが到着したときに、remove_signal_handler() が呼び出されます。最初に明示的に Remove_signal_handler() を呼び出します。) 同じシグナルに対して別のリターン関数を定義して、前のハンドラーを置き換えます (各シグナルはハンドラーとしてのみアクティブ化できます)。 。 sig パラメーターは、信号モジュールで定義された有効な信号値である必要があります。これにより、シグナルを処理できない場合は例外がスローされます。有効なシグナルではない場合、またはキャッチできないシグナル (SIGKILL など) の場合は、ValueError がスローされます。この特定のイベント ループ インスタンスがシグナルを処理できない場合 (シグナルはプロセッサごとのグローバル変数であるため、メイン スレッドのイベント ループのみがこれらのシグナルを処理できます)、RuntimeError がスローされます。

remove_signal_handler(sig)。設定されている場合は、シグナル sig のハンドラーを削除します。 add_signal_handler() と同じ例外をスローします (ただし、良好なシグナルを受信できない場合に RuntimeError を発生させる代わりに False を返す点が異なります)。ハンドラーが正常に削除された場合は True を返し、ハンドラーが設定されていない場合は False を返します。

標準インターフェースに準拠し、Future を返すいくつかのメソッド:

wrap_future(future)。 これには、PEP 3148 で説明されている Future (concurrent.futures.Future のインスタンスなど) と、イベント ループ互換の Future (tulip.Future のインスタンスなど) を返す必要があります。

run_in_executor(エグゼキュータ、コールバック、*引数)。エグゼキュータで callback(*args) を呼び出すように準備します (PEP 3148 を参照)。返された Future の成功結果が呼び出しの戻り値になります。 このメソッドは、wrap_future(executor.submit(callback, *args)) と同等です。 エグゼキュータがない場合は、デフォルトで 5 スレッドに設定される ThreadPoolExecutor が存在します。

set_default_executor(executor) run_in_executor() で使用されるデフォルトのエグゼキュータを設定します。

getaddrinfo(host, port, family=0, type=0, proto=0, flags=0)。socket.getaddrinfo() 関数と似ていますが、Future を返します。 Future の成功結果は、socket.getaddrinfo() の戻り値と同じ形式のデータ列です。 デフォルトの実装は run_in_executor() 経由でソケット.getaddrinfo() を呼び出しますが、他の実装では独自の DNS ルックアップの使用を選択する場合があります。オプションの引数はキーワード引数を指定する必要があります。

getnameinfo(sockaddr, flags=0)。socket.getnameinfo() と似ていますが、Future を返します。 Future の成功結果は (ホスト、ポート) の配列になります。 実装は、forgetaddrinfo() と同じです。

create_connection(protocol_factory, host, port, **kwargs) 指定されたホストとポートを使用してストリーミング接続を作成します。これにより、リンクを表すトランスポート依存の実装が作成され、protocol_factory() を呼び出してユーザーのプロトコル実装がインスタンス化 (または取得) され、この 2 つがバインドされます。 (以下のトランスポートとプロトコルの定義を参照してください。) ユーザーのプロトコル実装は、パラメーター (*) なしで protocol_factory() を呼び出すことによって作成または取得されます。戻り値は、成功した結果が (トランスポート、プロトコル) ペアである Future です。エラーにより成功したリンクが作成されなかった場合、Future には適切な例外セットが含まれます。 Future が完了すると、接続ハンドシェイクの完了時に発生するプロトコルの connection_made() メソッドは呼び出されないことに注意してください。

(*)protocol_factory がクラスである必要はありません。プロトコル クラスで定義済みのパラメーターをコンストラクターに渡す必要がある場合は、lambda または functool.partial() を使用できます。以前に構築した Protocol インスタンスのラムダを渡すこともできます。

オプションのキーパラメータ:

family、proto、flags: アドレスファミリー、プロトコル、および混合フラグパラメータは getaddrinfo() に渡されます。これらはすべてデフォルトで 0 に設定されます。 ((ソケット タイプは常に SOCK_STREAM です。)

ssl: True を渡して SSL トランスポート (デフォルトでプレーン TCP に設定することで確立されます) を作成します。または、 ssl.SSLConteext オブジェクトを渡して、デフォルトの SSL コンテキスト オブジェクトをオーバーライドします。

start_serving を使用します。 (protocol_factory, host, port, **kwds) は、接続を受信するループに入ります。ループがサービスに設定されると完了します。パラメータ (*) を指定しないと、protocol_factory は戻り値になります。プロトコルを作成するために呼び出され、接続のネットワーク側を表すトランスポートが作成され、protocol.connection_made(transport)

(*) を呼び出すことで 2 つのオブジェクトがバインドされます。 ただし、create_connection() に関する追加のメモは次のようになります。 、protocol_factory() は受信接続ごとに 1 回しか呼び出されないため、呼び出されるたびに新しい Protocol オブジェクトを返すことをお勧めします

オプションのキーパラメータ:

family、proto、フラグ: アドレスファミリー、プロトコル、および混合フラグ パラメーターは getaddrinfo() に渡されます。これらはすべてデフォルトで 0 に設定されます。 ((ソケット タイプは常に SOCK_STREAM です。)

追加: SSL をサポートしていますか? (SSL) を非同期的にサポートする方法がわかりません。これには証明書

が必要です。

追加: おそらく、Future の結果オブジェクトは、サービスの停止、すべてのアクティブな接続の終了、(サポートされている場合) バックログやその他のパラメーターの調整など、サービス ループの制御に使用できるでしょうか?アクティブな接続をクエリするための API を使用することもできます。また、エラーによりループの処理が停止した場合、またはループを開始できない場合は、完了したばかりの Future (サブクラス?) を返します。キャンセルするとループが停止する場合があります。

追加: 一部のプラットフォームでは、これらすべてのメソッドの実装に興味がない場合があります。たとえば、モバイル APP は start_serving() にあまり興味がありません。 (iPad に Minecraft サーバーがありますが...)

以下のファイル記述子のコールバック関数の登録方法は必須ではありません。これらのメソッドが実装されていない場合、これらのメソッドにアクセスすると (呼び出しではなく) AttributeError が返されます。デフォルトの実装ではこれらのメソッドが提供されますが、通常、ユーザーはこれらを直接使用せず、トランスポート層によってのみ使用されます。同様に、Windows プラットフォームでは、これらのメソッドが必ずしも実装されるわけではありません。選択イベント ループ モデルが使用されるか、IOCP イベント ループ モデルが使用されるかによって異なります。どちらのモデルも、 fileno() メソッドによって返されるオブジェクトの代わりに、整数のファイル記述子を受け入れます。ファイル記述子はクエリ可能であることが好ましいが、たとえば、ディスク ファイルはクエリ可能ではない。

add_reader(fd, callback, *args) ファイル記述子 fd が読み取り操作の準備ができたら、指定されたコールバック関数 callback(*args) を呼び出します。コールバック関数をキャンセルするために使用できるハンドラー関数オブジェクトを返します。 call_later() とは異なり、このコールバック関数は複数回呼び出すことができることに注意してください。同じファイル記述子に対して add_reader() を再度呼び出すと、以前に設定したコールバック関数がキャンセルされます。注: キャンセル ハンドラーは、ハンドラー関数が呼び出されるまで待機する場合があります。 fd を閉じたい場合は、remove_reader(fd) を呼び出す必要があります。 (TODO: ハンドラー関数が設定されている場合は、例外をスローします)。

add_writer(fd, callback, *args)。add_reader() と似ていますが、書き込み操作を実行する前にコールバック関数を呼び出します。

remove_reader(fd) ファイル記述子 fd に設定されている読み取り操作のコールバック関数を削除します。コールバック関数が設定されていない場合、何も動作しません。 (このような代替インターフェイスが提供されているのは、記録ファイル記述子が記録処理関数よりも便利で簡単であるためです)。削除が成功した場合は True を返し、失敗した場合は False を返します。

remove_writer(fd) ファイル記述子 fd に設定されている書き込み操作のコールバック関数を削除します。

未完成: ファイル記述子に複数のコールバック関数が含まれている場合はどうすればよいでしょうか?現在のメカニズムは以前のコールバック関数を置き換えるものであり、コールバック関数が登録されている場合は例外が発生する必要があります。

以下のメソッドはソケット非同期 I/O ではオプションです。これらは、上記のオプションのメソッドの代替であり、Windows のトランスポート実装で IOCP とともに使用することを目的としています (イベント ループがサポートしている場合)。ソケットパラメータはソケットをブロックしてはなりません。

sock_recv(靴下, n)。ソケットソケットからバイトを受信します。成功するとバイト オブジェクトとなる Future を返します。

sock_sendall(靴下、データ)。データバイトをソケットソケットに送信します。 Future を返します。成功すると、Future の結果は None になります。 (追加: sendall() または send() をエミュレートした方が良いでしょうか? しかし、sendall() は send() という名前にするべきだと思います?)

sock_connect(sock, address)。指定されたアドレスに接続します。 Future を返します。Future の成功結果は None です。

sock_accept(靴下)。ソケットからリンクを受け取ります。ソケットはリスニング モードであり、カスタム ソケットにバインドされている必要があります。 Future を返します。Future の成功結果は (conn,peer) の配列になります。ここで、conn は接続された非ブロッキング ソケット、peer はピア アドレスです。 (補足: この API スタイルは高レベルのサーバーでは非常に遅いと言われています。つまり、上記の start_sering() があります。これはまだ必要ですか?)

補足: オプションのメソッドはどれもあまり優れていません。おそらくこれらはすべて必要なのでしょうか?これは依然として、プラットフォームのより効率的なセットアップに依存しています。もう 1 つの可能性は、ドキュメントに、これらは「送信のみに利用可能」であり、他のものは「いかなる場合でも利用可能」であると記載されているということです。

コールバック順序


2 つのコールバック関数が同時にスケジュールされている場合、それらは登録された順序で実行されます。例:

ev.call_soon(foo)

ev.call_soon(bar)

foo() は bar() 内で実行されることが保証されています。

call_soon() が使用されている場合、システムクロックが逆方向に進んでも、この保証は依然として当てはまります。これは call_later(0,callback,*args) にも機能します。ただし、システム クロックが逆行しているときに call_later() を遅延ゼロで使用した場合、保証はありません。 (優れたイベント ループ実装では、システム クロックの逆行によって引き起こされる問題を回避するために time.monotonic() を使用する必要があります。PEP 418 を参照してください。)

コンテキスト


すべてのイベント ループにはコンテキストの概念があります。デフォルトのイベント ループ実装の場合、コンテキストはスレッドです。イベント ループの実装では、すべてのコールバックを同じコンテキストで実行する必要があります。イベント ループの実装では、一度に 1 つのコールバックのみを実行する必要があるため、コールバックは、同じイベント ループ内でスケジュールされた他のコールバックからの自動相互排他を維持する責任があります。

異常


Python には 2 種類の例外があります。それは、Exception クラスから派生したものと、BaseException から派生したものです。 Exception から派生した例外は通常、キャッチされて適切に処理されます。たとえば、例外は Future を介して渡され、コールバック内で発生した場合はログに記録されて無視されます。

ただし、BaseException からの例外は決してキャッチされず、通常はエラー トレースバックが伴い、プログラムが終了します。 (この例には KeyboardInterrupt や SystemExit が含まれます。これらの例外を他のほとんどの例外と同じように扱うのは賢明ではありません。)

Handler クラス


コールバック関数を登録するためのさまざまなメソッド (call_later() など) があり、登録を表すオブジェクトを返します。変更されたオブジェクトはコールバック関数をキャンセルするために使用できます。ユーザーはこのクラスをインスタンス化する必要はありませんが、このオブジェクトに Handler という適切な名前を付ける必要があります。このクラスにはパブリック メソッド

cancel() があります。コールバック関数をキャンセルしてみます。 補足: 正確な仕様。

読み取り専用パブリックプロパティ:

コールバック。 呼び出されるコールバック関数。

引数。コールバック関数を呼び出すためのパラメータの配列。

キャンセルされました。 cancel() テーブルが呼び出された場合、その値は True になります。


一部のコールバック関数 (call_later() を通じて登録された関数など) は 1 回だけ呼び出されるように意図されていることに注意してください。その他 (add_reader() 経由で登録されたものなど) は複数回呼び出されるように意図されています。

補足:コールバック関数を呼び出すためのAPI(例外処理のカプセル化は必要か)?何回呼び出されたかを記録する必要がありますか?おそらくこの API は _call_() のようなものでしょうか? (ただし、例外は抑制されるはずです。)

追加: コールバック関数がディスパッチされたときに、それらのリアルタイム値を記録するためのパブリック プロパティはありますか? (これには、ヒープに保存する何らかの方法が必要なためです。)

Futures


ulip.Future は、PEP 3148 の concurrent.futures.Future に似るように意図的に設計されていますが、わずかな違いがあります。この PEP で Future が言及されている場合は、concurrent.futures.Future として明示的に指定されていない限り、tulip.Future を指します。 tulip.Future がサポートするパブリック API は次のとおりであり、PEP 3148 との違いも指摘されています:

cancel() Future が完了した (またはキャンセルされた) 場合、False が返されます。それ以外の場合は、Future の状態をキャンセルされた状態 (完了したとも解釈できます) に変更し、コールバック関数をスケジュールし、True を返します。

canceled()。Future がキャンセルされた場合は True を返します。

running() は常に False を返します。 PEP 3148 とは異なり、実行状態はありません。

done()。Future が完了している場合は True を返します。注: キャンセルされた将来も完了したとみなされます (これはここでも他の場所でも当てはまります)。

result()。set_result() によって設定された結果を返すか、set_Exception() によって設定された例外を返します。キャンセルされた場合は、CancelledError がスローされます。 PEP 3148 とは異なり、タイムアウト パラメータや待機はありません。 Future がまだ完了していない場合は、例外がスローされます。

Exception()。上記と同様に、例外を返します。

add_done_callback(fn)。Future が完了した (またはキャンセルされた) ときに実行するコールバック関数を追加します。 Future が完了した (またはキャンセルされた) 場合、call_soon() を使用してコールバック関数がディスパッチされます。 PEP 3148 とは異なり、追加されたコールバック関数はすぐには呼び出されず、常に呼び出し元のコンテキストで実行されます。 (通常、コンテキストはスレッドです)。 call_soon() を使用してコールバック関数を呼び出すと理解できます。注: 追加されたコールバック関数 (この PEP の他のコールバック関数とは異なり、以下の「コールバック スタイル」セクションの規則を無視します) は常に Future をパラメーターとして受け取ります。このコールバック関数は Handler オブジェクトであってはなりません。

set_result(result)。この Future は完了 (またはキャンセル) 状態にはなりません。このメソッドは、現在の Future を完了状態にし、関連するコールバック関数を呼び出す準備をします。 PEP 3148 とは異なり、これはパブリック API です。

set_Exception(例外)。上記と同様に、例外を設定します。

内部メソッド set_running_or_notify_cancel() はサポートされなくなりました。実行状態に直接設定する方法はありません。

この PEP は次の例外を定義します:

InvalidStateError。この例外は、呼び出されたメソッドがこの Future の状態を受け入れない場合にスローされます (例: 完了した Future で set_result() メソッドを呼び出した場合、または result( ) メソッドは未完成の Future に対して呼び出されます)。

InvalidTimeoutError。この例外は、result() またはException() の呼び出し時にゼロ以外の引数が渡された場合にスローされます。

CancelledError。concurrent.futures.CancelledError のエイリアス。この例外は、キャンセルされた Future に対して result() メソッドまたはException() メソッドが呼び出されたときにスローされます。

TimeoutError。 concurrent.futures.TimeoutError のエイリアス。 EventLoop.run_until_complete() メソッドによってスローされる可能性があります。

Future を作成すると、デフォルトのイベント ループに関連付けられます。 (まだ行われていません: イベント ループを引数として渡すことを許可しますか?)。

concurrent.futures パッケージの wait() メソッドと as_completed() メソッドは、tulip.Future オブジェクトをパラメーターとして受け入れません。ただし、以下で説明する同様の API tulip.wait() と tulip.as_completed() があります。

Tulip.Future オブジェクトは、サブルーチン (コルーチン) の式からの生成に適用できます。これは、Future の __iter__() インターフェイスを通じて実装されます。以下の「サブルーチンとスケジューラ」セクションを参照してください。

Future オブジェクトがリサイクルされるとき、関連する例外があるが、result()、Exception()、または __iter__() メソッドが呼び出されない場合 (または例外が生成されてもまだスローされていない場合)、例外は発生する必要があります。ログに記録する必要があります。 TBD: どのレベルで録音されますか?

将来的には、tulip.Future と concurrent.futures.Future を統合する可能性があります。たとえば、後者のオブジェクトに __iter__() メソッドを追加して、式からの生成をサポートします。イベント ループが未完了の result() を誤って呼び出してイベント ループをブロックしないようにするために、ブロック メカニズムは現在のスレッドにアクティブなイベント ループがあるかどうかを検出する必要があります。検出できない場合は例外がスローされます。ただし、この PEP は外部依存関係を最小限に抑えることを目的としているため (Python3.3 のみに依存する)、現時点では concurrent.futures.Future には変更が加えられません。

トランスポート層


トランスポート層は、ソケットまたは他の同様のメカニズム (パイプや SSL 接続など) に基づく抽象化層を指します。ここのトランスポート層は、Twisted と PEP 3153 の影響を大きく受けています。ユーザーがトランスポート層を直接実装またはインスタンス化することはほとんどありません。イベント ループは、トランスポート層を設定するための関連メソッドを提供します。

トランスポート層はプロトコルを操作するために使用されます。一般的なプロトコルは、基礎となるトランスポート層の特定の詳細を気にせず、トランスポート層を使用してさまざまなプロトコルを操作できます。たとえば、HTTP クライアント実装では、通常のソケット トランスポート層を使用することも、SSL トランスポート層を使用することもできます。プレーン ソケット トランスポート層は、HTTP プロトコル以外の多数のプロトコル (SMTP、IMAP、POP、FTP、IRC、SPDY など) で動作できます。

ほとんどの接続には非対称の特性があります。通常、クライアントとサーバーは異なる役割と動作を持ちます。したがって、トランスポート層とプロトコル間のインターフェイスも非対称になります。プロトコルの観点から見ると、データの送信は、トランスポート層オブジェクトの write() メソッドを呼び出すことによって実行されます。 write() メソッドは、データをバッファに入れた直後に戻ります。データを読み取るとき、トランスポート層はより積極的な役割を果たします。ソケット (または他のデータ ソース) からデータを受信した後、トランスポート層はプロトコルの data_received() メソッドを呼び出します。

トランスポート層には次のパブリック メソッドがあります:

write(data)。パラメータはバイト オブジェクトである必要があります。なしを返します。トランスポート層はデータのバイトを自由にキャッシュできますが、データが相手側に送信され、データ フローの動作が維持されることを保証する必要があります。つまり、 t.write(b'abc'); t.write(b'def') は t.write(b'abcdef') と同等であり、

t.write(b'a') とも同等です。

t.write(b'b')

t.write(b'c')

t.write(b'd')

t.write(b'e')

t.write( b'f')

writelines(iterable) と同等:

iterable:

self.write(data)

write_eof() のデータ接続を閉じると、write を呼び出すことができなくなります。 ( ) 方法。バッファリングされたデータがすべて転送されると、トランスポート層はもうデータがなくなったことを相手に通知します。一部のプロトコルはこれをサポートしていません。その場合、write_eof() を呼び出すと例外がスローされます。 (注: このメソッドは以前はhalf_close() と呼ばれていました。特定の意味を知らない限り、このメソッド名はどちらの端が閉じられるかを明確に示しません。)

can_write_eof() プロトコルが write_eof() をサポートしている場合は、True を返します。それ以外の場合は False を返します。 (write_eof() が利用できない場合、一部のプロトコルは対応する動作を変更する必要があるため、このメソッドが必要になります。たとえば、HTTP では、現在のサイズが不明なデータを送信するために、通常、write_eof() が使用されます。データは送信されましたが、SSL はこの動作をサポートしません。対応する HTTP プロトコル実装ではチャンク エンコーディングを使用する必要があります。ただし、送信時にデータ サイズが不明な場合、どちらの場合にも最善の解決策はコンテンツを使用することです。 -Length ヘッダー)

pause() 。データの送信を一時停止し、resume() メソッドを直接呼び出します。一時停止() 呼び出しと再開() 呼び出しの間では、プロトコルの data_received() メソッドが再度呼び出されることはありません。 write() メソッドでは無効です。

resume()。プロトコルの data_received() を使用してデータ送信を再開します。

close() 接続を閉じます。 write() を使用してバッファされたすべてのデータが送信されるまで、接続は閉じられません。接続が閉じられると、プロトコルの data_received() メソッドは呼び出されなくなります。バッファリングされたデータがすべて送信されると、プロトコルの connection_lost() メソッドがパラメータとして None を指定して呼び出されます。注: このメソッドは、上記のすべてのメソッドが呼び出されることを保証するものではありません。

abort()。接続を中止します。バッファ内のまだ転送されていないデータはすべて破棄されます。そのすぐ後に、プロトコルの connection_lost() が呼び出され、None パラメーターが渡されます。 (未定: close()、abort()、または相手側の終了アクションで、異なるパラメータを connection_lost() に渡しますか? それとも、これをクエリするための特別なメソッドを追加しますか? Glyph は、別の例外を渡すことを推奨します)

未完成: フロー制御の別の方法を提供します。バッファ データが負担になる場合、トランスポート層はプロトコル層を一時停止できます。推奨事項: プロトコルにポーズ() メソッドと再開() メソッドがある場合、それらが存在しない場合はトランスポート層がそれらを呼び出せるようにします。プロトコルはフロー制御をサポートしません。 (pause()とresume()については、プロトコル層とトランスポート層で別の名前を使用した方が良いでしょうか?)

プロトコル


プロトコルは、通常、トランスポート層と一緒に使用されます。ここでは、一般的に使用されるいくつかのプロトコル (たとえば、いくつかの便利な HTTP クライアントとサーバーの実装) が提供されていますが、そのほとんどは、ユーザーまたはサードパーティのライブラリを実装する必要があります。

プロトコルは、トランスポート層によって呼び出される次のメソッドを実装する必要があります。これらのコールバック関数は、正しいコンテキストでイベント ループによって呼び出されます (上記の「コンテキスト」セクションを参照)。

connection_made(transport) は、トランスポート層が準備ができており、実装のもう一方の端に接続されていることを意味します。プロトコルはトランスポート層の参照を変数として保存する必要があり (その write() や他のメソッドを後で呼び出せるようにするため)、この時点でハンドシェイク要求を送信することもできます。

data_received(data)。トランスポート層がいくつかのデータを読み取りました。パラメータは空ではないバイト オブジェクトです。このパラメータのサイズには明示的な制限はありません。 p.data_received(b'abcdef') は、相手側で呼び出される

p.data_received(b'abc')

p.data_received(b'def')

eof_received() のステートメントと同等である必要があります。このメソッドは、_eof() またはその他の同等のメソッドを記述するときに呼び出されます。デフォルトの実装は、トランスポート層の close() メソッドを呼び出します。このメソッドは、プロトコルの connection_lost() メソッドを呼び出します。

connection_lost(exc)。トランスポート層が閉じられたか中断されました。もう一方の端が接続を安全に閉じたか、例外が発生しました。最初の 3 つのケースでは、パラメーターは None です。例外の場合、パラメーターはトランスポート層の中断を引き起こした例外です。 (未定: 最初の 3 つのケースは別の方法で処理する必要がありますか?)

これは呼び出しの順序と多様性を示す図です:

connection_made()-- 1 回だけ

data_received()-- 0 回以上回

eof_received()-- 多くても 1 回

connection_lost()-- 正確に 1 回

補足: プロトコルとトランスポート プロトコルが時期尚早に GC (ガベージ コレクション) にならないように、ユーザーのコードで何らかの処理を行う必要があるかどうかを議論します。

コールバック関数のスタイル


ほとんどのインターフェイスはコールバック関数を受け取り、位置パラメータも受け取ります。たとえば、foor("abc",42) がすぐに呼び出されるようにスケジュールするには、ev.call_soon(foo,"abc",42) を呼び出します。 foo() への呼び出しをスケジュールするには、ev.call_soon(foo) を使用します。この規則により、一般的なコールバック関数プログラミングに必要な小さなラムダ式の数が大幅に削減されます。

この規則は、キーワード パラメーターを明示的にサポートしません。キーワード引数は、コールバック関数に関するオプションの追加情報を渡すためによく使用されます。これにより、呼び出し元によってキーワードがどこかで宣言されているかどうかを気にすることなく、API をエレガントに変更できます。キーワード引数を使用して呼び出す必要があるコールバック関数がある場合は、ラムダ式または functools.partial を使用できます。例:

ev.call_soon(functools.partial(foo, "abc",repeat=42))


イベントループ実装を選択してください

完了します。 (select/poll/epollの使い方と選択の変更方法について。イベントループ戦略に属する)

コルーチンとスケジューラー


イベントループとは状態が異なるため、独立したトップレベルの部分です。インターフェース。コルーチンはオプションであり、コールバックを使用してコードを記述するだけで済みます。一方、スケジューラ/コルーチン API 実装は 1 つだけであり、コルーチンを選択した場合は、それのみが使用されます。

Coroutine


コルーチンは、次の規則に従うプロデューサーです。適切なドキュメントを作成するには、すべてのコルーチンを @tulip.coroutine で修飾する必要がありますが、これは厳密に必須ではありません。

コルーチンは、元の yield 構文の代わりに、PEP 380 で導入された yield from 構文を使用します。

「コルーチン」という言葉は「プロデューサー」と似た意味を持ち、2 つの異なる (関連性はあるものの) 概念を説明するために使用されます。

コルーチンの関数を定義します (tulip.coroutine 装飾を使用して定義された関数)。曖昧さを避けるために、この関数をコルーチン関数と呼ぶことができます。

コルーチンを通じてオブジェクトを取得します。このオブジェクトは、最終的に完了する計算または I/O 操作 (通常は両方) を表します。曖昧さを排除するために、このオブジェクトをコルーチン オブジェクトと呼ぶことができます。


コルーチンでできること:

result = Future からの収量を使用します -- Future が完了するまでコルーチンを一時停止し、その後 Future の結果を返すか、渡したい例外をスローします。

result = コルーチンからの yield を使用します -- 別のコルーチンが結果を生成する (または渡される例外をスローする) のを待ちます。このコルーチンの例外は、別のコルーチンの呼び出しである必要があります。

結果を返す -- 結果を待機するために yield from 式を使用するコルーチンの結果を返します。

例外をスローする -- 待機するために yield from 式を使用する (プログラム) のコルーチンで例外をスローします。

コルーチンを呼び出しても、そのコードはすぐには実行されません。コルーチンは単なるプロデューサーであり、呼び出しによって返されるコルーチンは確かに単なるプロデューサー オブジェクトであり、反復処理するまで何も行いません。コルーチンの場合、実行を開始するには 2 つの基本的な方法があります。1 つは別のコルーチンから yield を呼び出すか (他のコルーチンがすでに実行されていると仮定します)、もう 1 つはタスクに変換する方法です (以下を参照)。

コルーチンは、イベント ループの実行中にのみ実行できます。

複数のコルーチンの待機


concurrent.futures パッケージ内の API は、複数のコルーチンまたは Future を待機するために提供されます:

2 つの類似した wait() と as_completed() です。なし、return_when=ALL_COMPLETED)。 これは、Future または他のコルーチンが完了するのを待つ fs によって提供されるコルーチンです。コルーチン パラメーターはタスクにカプセル化されます (以下を参照)。このメソッドは Future を返します。Future の成功した結果は、2 つの Future セット (done、pending) を含むタプルです。done は元の Future (またはカプセル化されたコルーチン) を意味し、pending は残りを意味します。未完了(またはキャンセル)など。オプションのパラメータ timeout および return_when は、 concurrent.futures.wait() のパラメータと同じ意味およびデフォルト値を持ちます。 timeout が None でない場合は、すべての操作のタイムアウトを指定します。 return_when は、いつ停止するかを指定します。定数 FIRST_COMPLETED、FIRST_EXCEPTION、ALL_COMPLETED は同じ値定義を使用し、PEP 3148 で同じ意味を持ちます:

ALL_COMPLETED(デフォルト): すべての Future が処理または完了するまで (またはタイムアウトが発生するまで) 待機します。

FIRST_COMPLETED: 少なくとも 1 つの Future が完了またはキャンセルされるまで (またはタイムアウトが発生するまで) 待機します。

FIRST_EXCEPTION: 例外により、少なくとも 1 つの Future が準備できる (キャンセルされていない) まで待機します (キャンセルされた Future をフィルターから除外するのは魔法ですが、PEP 3148 ではこの方法で行われます。)

tulip.as_completed(fs, timeout=None)、Future の値を持つイテレータを返します。次の Future またはコルーチンが fs から完了するまで、成功した値を待ち、それ自体の結果を返します (またはその例外をスローします)。オプションのパラメータ timeout は、concurrent.futures.wait() のパラメータと同じ意味とデフォルト値を持ちます。タイムアウトが発生すると、反復子によって返される次の Future は待機中に TimeoutError 例外をスローします。 使用例:

for f in as_completed(fs):

result = yield from f # 例外が発生する可能性があります。


# 独立して実行されるサブプログラムを管理するオブジェクトです。 Task インターフェイスと Future インターフェイスは同じです。サブルーチンが完了するか例外をスローすると、それに関連付けられたタスクも完了します。返される結果は対応するタスクの結果であり、スローされる例外は対応するタスクの例外です。

未完了のタスクをキャンセルすると、関連するサブルーチンの実行が続行できなくなります。この場合、サブルーチンはキャンセル コマンドを適切に処理するために例外を受け取ります。もちろん、サブルーチンはこの例外を処理する必要はありません。このメカニズムは、PEP 342 で説明されているジェネレーターの標準 close() メソッドを呼び出すことによって実現されます。

タスクは、サブルーチンと Twisted などのコールバックベースのフレームワーク間の対話にも役立ちます。サブルーチンをタスクに変換した後、タスクにコールバック関数を追加できます。

なぜすべてのサブルーチンをタスクに変換しないのかと疑問に思うかもしれません。 @tulip.coroutinedecorator はこのタスクを実現できます。この場合、サブルーチンが別のサブルーチン (または他の複雑な状況) を呼び出すと、プログラム全体が非常に遅くなります。複雑な状況では、サブルーチンをタスクに変換するよりも保持した方がはるかに高速です。

スケジューラ

スケジューラにはパブリックインターフェイスがありません。将来およびタスクの歩留まりを使用して、スケジューラと対話できます。実際、スケジューラには特定のクラスの実装がありません。 Future クラスと Task クラスは、イベント ループのパブリック インターフェイスを使用してスケジューラの動作を表します。したがって、サードパーティのイベント ループ実装に変更した場合でも、スケジューラー機能は引き続き使用できます。

Sleep


Unfinished: yield sleep(秒) スリープ(0) を使用して I/O を一時停止およびクエリできます。

コルーチンとプロトコル


コルーチンを使用してプロトコルを実装する最良の方法は、ストリーム キャッシュを使用してデータを埋めることです。また、read(n) や readline( などのメソッドも使用できます)。 ) や、Future を返してデータを非同期に読み取るその他のメソッド。接続が閉じられると、read() メソッドは結果が '' となる Future を返すか、例外により connection_closed() が呼び出された場合は例外をスローする必要があります。

データを書き込むには、トランスポート上の write() (および同様の) メソッドを使用できます。これらは Future を返しません。 connection_made() が呼び出されたときにコルーチンをセットアップして開始するための標準プロトコル実装を提供する必要があります。

追加: さらなる仕様。

キャンセル

補足。タスクがキャンセルされると、そのコルーチンは、スケジューラから中止される場合は常に例外を認識します (操作の途中で中止される可能性があるなど)。どの例外をスローするかを明確にする必要があります。

再度追加: タイムアウト。

既知の問題


APIをデバッグしますか? たとえば、大量のものを記録したり、珍しい状況 (キューが空になるより早くいっぱいになるなど) を記録したり、さらには非常に時間がかかるコールバック関数を記録したりするものです...

内省的な API は必要ですか?たとえば、リクエスト読み取りコールバック関数はファイル記述子を返します。または、次にスケジュールされた (コールバック関数) が呼び出されたとき。または、コールバック関数によって登録された一部のファイル記述子。

トランスポートには、ソケットのアドレスを返そうとするメソッド (および同等のアドレスを返す別のメソッド) が必要な場合があります。これはソケットのタイプによって異なりますが、必ずしもソケットであるとは限りません。その場合は None が返されるはずです。 (あるいは、ソケット自体を返すメソッドも考えられますが、IP リンクの実装にソケットを使用しない可能性もあります。では、どのように行うのでしょうか?)

os.fokd() を処理する必要があります。 (Tulip の場合、これはおそらくセレクター クラスに行きます。)

おそらく start_serving() には現在のソケットに渡すメソッドが必要です (たとえば、gunicorn にはこれが必要です)。 create_connection() にも同じ問題があります。

いくつかの明示的なロックを導入することもありますが、 with lock: ブロック構文を使用できないため、使用するのが少し面倒です (ロックを待機するには、 yield from を使用する必要がありますが、これは with ステートメントでは不可能です) )。

データグラムプロトコルとリンクをサポートするかどうか。 sock_sendto() や sock_recvfrom() など、さらに多くのソケット I/O メソッドが存在する可能性があります。あるいは、ユーザークライアントが独自に作成することもあります (これはロケット科学ではありません)。 write()、writelines()、data_received() を単一のデータグラムにオーバーライドする理由はありますか? (Glyph は後者を推奨しています。) では、write() の代わりに何を使用すればよいでしょうか?最後に、コネクションレス データグラム プロトコルをサポートする必要があるでしょうか? (これは、sendto() と recvfrom() をカプセル化することを意味します。)

さまざまなスーパーマーケットを制御するために API が必要になる場合があります。たとえば、DNS、接続、SSL ハンドシェイク、アイドル接続、さらには各セッションの解決にかかる時間を制限したい場合があります。おそらく、一部のメソッドに timeout キーワード引数を完全に追加する方法や、call_later と Task.cancel() を介してタイムアウトを賢く実装する方法があるかもしれません。ただし、デフォルトのタイムアウトを必要とするメソッドが存在する可能性があり、この仕様のグローバル操作のデフォルト値を変更する必要がある場合があります。 (例: すべてのイベントループ)。

NodeJS スタイルのイベントトリガー?それともこれを別のチュートリアルにしますか?これは実際にはユーザー空間で行うのが十分簡単ですが、標準化で行う方がよいかもしれません (https://github.com/mnot/thor/blob/master/thor/events.py および https://github を参照) .com /mnot/thor/blob/master/doc/events.md など)


参考文献 TBD の


PEP 380: yield のセマンティクスを説明する Greg Ewing のチュートリアル。

PEP 3148 は concurrent.futures.Future.

PEP 3153 について説明していますが、拒否されましたが、トランスポートとプロトコルを分離する必要性についてうまく説明しています。

Tulip リポジトリ: http://code.google.com/p/tulip/

Nick Coghlan による優れたブログ エントリ。非同期 I/O、gevent、および Future の使用方法のさまざまな処理に関する背景が記載されています。コンセプトは好きです。 wihle、for、with、: http://python-notes.boredomandlaziness.org/en/latest/pep_ideas/async_programming.html

未定: Twisted、Tornado、ZeroMQ、pyftpdlib、libevent、libev、pyev、libuv、について

謝辞


PEP 3153 に加えて、影響を受けるものには、PEP 380 と、チュートリアルからの Greg Ewing の成果物、Twisted、Tornado、ZeroMQ、pyftpdlib、tulip (すべてをまとめようとする著者の試み) が含まれます。 、wattle (Steve Dower による対案)、2012 年 9 月から 12 月にかけての Python アイデアに関する広範な議論、Steve Dower および Dino Viehland、Ben との Skype セッション、Darnell との電子メール交換、Niels Provos との聴衆 (オリジナル) libevent の著者)、そして Glyph、Brian Warner、David Reid、Duncan McGreggor を含む数名の Twisted 開発者との 2 回の対面ミーティング。同様に、著者は初期段階で Google App Engine の NDB ライブラリの非同期サポートにも重要な影響を与えました。


声明:
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。