これだと、単純に関数を再帰させるのと何が違うんだって思いますよね。ひと工夫してみましょう。2あまり大きな値は入力しないように(ハロー地獄のはじまりです)。挨拶して、またその仕事をイベントループに押し付けるウルサイやつがいることと、あとは少しprint文を増やしたくらいです。この仕事をrandom_hitの仕事依頼の次の行で頼みました。一応言っておくと、複数の仕事がある場合は先に追加したほうから処理していきます(FIFOってやつですね)。していないにもかかわらず、eternal_helloが挨拶してきました!とはいえ、そんな不思議でもありませんよね。仕事の処理はFIFOで、random_hitの次の仕事は先約のeternal_helloの後に行われるはずですから。してから実際にイベントループが停止するまでの挙動を少し見てみましょう。と続き、そして結果が出力されています。驚くことでもありませんが、Futureオブジェクトに追加した完了時コールバックは、値が設定されたときに、仕事としてイベントループに押し付けられます。run_until_complete()は内部でloop.stop()を呼ぶといいましたが、これもFutureオブジェクトの完了時コールバックとして呼ばれます。真相はこうなります:Hit!:このときにFutureオブジェクトは完了し、コールバックをイベントループに仕事として渡す最初のHello!:コールバックより先約のeternal_helloが挨拶し、また挨拶の仕事を押し付けるDONE!:先約の仕事がすべて終わり、完了時コールバックが実行される一連の完了時コールバックの最後のタイミングでloop.stop()が呼ばれる。2番目のところで押し付けられた挨拶の仕事を残業としてこなす。このとき生じた新たな挨拶の仕事は次回実行時に持ち越し。残業おわり!イベントループは無駄な残業をせずにとっとと帰るのであった。asyncioモジュールが「イベントループに仕事を依頼していく」という形で動いているというのが分かりますね。最後の例では、ほんの少しだけですがイベントループの器用さが垣間見れました。「仕事を中断して別の仕事に取りかかる」という器用さの真髄はお目にかかれていません。私たちは未だ、そういう風な処理ができるような形の仕事をループちゃんに与えていないからです。ループちゃんの本領が見れそうですよ!コルーチンとタスクお次はコルーチンです。コルーチンはジェネレータを定義するときと同じように、関数定義に近い形で書きます。print("Hello!")このとき、hello()はコルーチンオブジェクトです。ジェネレータと似てますね*1。関数みたいなものです。ジェネレータと似ているところがもう一つあります。[coroutine]によってコルーチンをチェーンすることができます。[future]という形式です。順を追って話しましょう。イベントループにコルーチンを仕事として渡すには、関数のときとは異なる手順を踏みます。コルーチンは、FutureのサブクラスであるTaskオブジェクトにくるまれて、イベントループと関わり合いをもちます。Futureオブジェクトを渡した場合は、そのオブジェクトがそのまま返ってきます(Taskオブジェクトにはならないことに注意!)。loop.run_until_complete()にはコルーチンを渡すこともできます。そのとき、コルーチンは内部でensure_future()によってTaskオブジェクト化されます。いうわけで、Taskオブジェクトを作るときにはコルーチンを渡します。Taskオブジェクトはこのときに「タスクのステップ」という仕事を(今までやってきたようにcall_soonを使って)イベントループに頼みます。[Future]にぶつかるまで進めます。コルーチンにFutureオブジェクトへのawaitがなかった場合は、ふつうにコルーチンの最後まで実行され、その返り値がTaskオブジェクトの結果として設定され、完了します(TaskはFutureのサブクラスであることに注意)。Futureオブジェクトへのawaitがあった場合、コルーチンはジェネレータのyieldよろしく、その時点で一旦停止します。そのFutureオブジェクトに完了時コールバックとして「タスクのステップ」を追加します。次回、コルーチンは一旦停止したところからFutureオブジェクトの結果を受け取って再開します。これが「タスクのステップ」という仕事の概要です。この妙技の仕組みは、もうお分かりになるかと思います。このTaskオブジェクトの続きは、Futureオブジェクトが完了しないとどうすることもできないわけですから、これは実に理にかなっています。例として、await対象としてよく使われるasyncio.sleepと似たようなコルーチンを書いてみましょう。h.cancel()このコルーチンは内部でFutureオブジェクトをつくり、delay秒後にresultをそのFutureオブジェクトにセットするようcall_laterしています。完了するまでイベントループから遠ざけます。futureの完了はもちろん、delay秒後に完了します!実際にasyncio.sleepを使ってみましょう。以下の例は割とどこででも見るものです。:)asyncio.sleepがつくったFutureオブジェクトが0.2秒後に完了し、それによってコルーチンが再開します。それがn回続いたあと、コルーチンが終わることでTaskオブジェクトが完了します。ちなみに、同じ秒数のcall_laterが複数あった場合、その順序はよく分かりません。カウント1以降は出力の順番はばらばらになっています。ところで、なんだか見慣れないloop.run_until_complete(asyncio.wait(futures))によってループを実行していますね。asyncio.gatherasyncio.wait(futures)はコルーチンで、デフォルトでは「引数の中のFutureオブジェクトがすべて完了したら完了する」ようなコルーチンです。今回の実行は「futuresの中のコルーチンが全部終わるまでループを実行し続ける」という条件になっています。ちなみに、waitは他にも「どれか1つでも完了すれば完了」「例外を誰かが吐いたら完了」という風な条件も設定できます。tweakloop.run_until_complete(asyncio.wait(futures))loop.close()としても大丈夫です。waitは返り値として「完了したFutureオブジェクトの集合」と「未完のFutureオブジェクトの集合」のタプルを返してくれます。集合ですから、渡したシーケンスの順序は保たれないと思った方がいいでしょう。渡したシーケンスの順序を保ってほしい場合はasyncio.gatherの出番です。あ、安心してください!もちろんコルーチンを渡しても、Taskオブジェクトにくるんでくれますよ!結果が欲しい場合はgatherのほうがいいかもしれませんね。n秒はかかりそうな気もします。ならず、イベントループはasyncio.sleepのところでブロッキングされずに他の仕事を進めることができます。10これは少し前にやったrandom_hitのコルーチン版です。loop.run_until_complete(random_hit(n))たった3行!もちろん、nの値の設定を関数化したこともありますが、見てほしいのは断然ループとかの方です。イベントループを取得し、random_hit(n)コルーチンが終わるまでループを走らせる。それしかしていません。何より変わったのは、Futureオブジェクトをコルーチン内部で用意しているというところです。loop.run_until_completeがコルーチンの値を返してくれるので、Futureオブジェクトをこっち側が保持しておく必要がなくなりました。確かに、Futureオブジェクトを内部で用意するために、内部に関数定義を持たなければならないようにもなりましたが、コルーチン使用者側からすればFutureオブジェクトの世話をしなくてよくなり、ぐっと使いやすくなりました。感じているだろうとは思いますが、asyncioの実際上の基本ユニットはコルーチンです。いうわけで、ここからは積極的にコルーチンを使っていくことにしましょう。I/Oに向かってasyncio.sleepはよくasyncioモジュールにおけるブロッキングの代わりとして用いられますが、実態はただのcall_laterです。asyncioモジュールは、その名が本当なら非同期I/Oのためのモジュールなはずです。I/Oが実際にどうやって非同期になされるのかというのをみないと、このモジュールを理解したことにはならないでしょう。Pythonで扱えるI/Oは、そりゃたくさん様々にあります。標準I/Oに、ファイルI/Oに、それから、今から嫌なくらいに見ていくプロセス間通信です。asyncioモジュールがターゲットとしているのはどうやらプロセス間通信のようです。それもそのはずで、そもそもasyncioモジュールの成し遂げるシングルスレッドでの非同期な処理形態というのは、レイテンシのある通信を一度にたくさんしたいときに使えるワザだからです。いうわけで、以下ではI/Oの中でも、プロセス間通信について見ていきます(ちなみに、標準I/OやファイルI/Oもちょっと細工をしてあげれば、イベントループはブロッキングせずに上手く扱ってくれるようになります。いや、んー、まあ、この場合はイベントループがすごいというよりは、マルチスレッドがすごいということになるんですけど、詳しくは後で話します)。明かりを灯そう:ソケット低水準な通信方法にはいくつかありますが、ここではソケットだけを取り上げることにします(他のについてはドキュメントを見てください)。http://docs.python.jp/3/library/asyncio-eventloop.html#low-level-socket-operationshttp://docs.python.jp/3/library/asyncio-eventloop.html#watch-file-descriptorsとはいえ、そもそも、ソケットのノンブロッキングな扱い方といえば、既にselectorを用いた方法があります!イベントループがこれを利用しない手はありませんし、実際そうしています。低水準なソケットの操作は、そのselectorにソケットを登録し、読み書き可能になり次第、登録時に渡しておいたコールバック関数で処理するようにします。ファイルディスクリプタですが、ソケットをそのまま渡しても大丈夫です。肝心の読み書きは次のメソッドで行います。data)ソケットからデータが欲しいようなコルーチンは、Futureオブジェクトを作り、そのFutureオブジェクトをコールバックの引数として渡しておき、コールバックの中で読み出したデータをFutureオブジェクトにセットしてもらえばよさそうです。address):add_writerのコールバック内でloop.sock_accept(sock):add_readerのコールバック内で低水準ソケットを使いたいけれど、ソケット自体についてあまり知らないという人は、HOWTOをひとまず読むことをおすすめします。その気がないのであれば、低水準においてノンブロッキングI/Oが可能だということを知ってほしかっただけなので、以下は飛ばしてもらっても構いません。*args))コールバック関数中で、ソケットを受け入れ(loop.sock_accept(sock))とすればいいでしょう。上ではIPv4アドレスファミリーを用いたTCPタイプのソケットを例に挙げましたが、IPv6やBluetoothアドレスファミリーを用いたり、UDPタイプや生タイプのソケットをつくることもできます。socketモジュールにあるAF_*定数はアドレスファミリーを表し、SOCK_*定数はソケットタイプを表します。詳しい内容はドキュメントをどうぞ。http://docs.python.jp/3/library/socket.html#constantsもっと高水準を!:トランスポート/プロトコルすべてのプロセス間通信はソケットに通ずる......とまでは言いませんが、大抵のプロセス間通信は確かにソケットでできそうです。おさらいすると、「コールバックを持たせてループに預ける」だけ。非常に簡単な方法で非同期なプロセス間通信が実現できたわけですが、接続、読み書き、切断という一連の処理をそれぞれのコールバック関数に書くことになるので、あるソケットの一生を追うのにお目々が行ったり来たり、少し面倒くさそうです。通信の一連の流れ、要はどんなコールバックがそれぞれの段階で呼ばれるのか、というのは一箇所に書いてあったほうが他の誰かにとって、そして未来のあなたにとっても親切でしょう。いうわけで、asyncioモジュールのトランスポート/プロトコルの出番です。プロトコルは、ある通信オブジェクト(≒ソケット)の一連の流れ(接続、読み書き、切断)をメソッドとして記述するためのクラスで、まさに今の私たちが求めているものです。簡単に説明すると、プロトコルは所定のデータ様式を扱うための処理(所定のデータ様式のアレコレ)を担い、トランスポートは実際のI/Oやバッファリング(I/O現場のアレコレ)を担います。プロトコルファクトリーは各接続ごとにプロトコルインスタンスを新たに作って提供してくれます。サブプロセスパイプのトランスポートを実装しています。通信において実際に相手とやりとりするのがトランスポートの役目です。各種トランスポートはそれぞれの通信の形態に対応しており、相応のメソッド群を持ち合わせています。以下にトランスポートを列挙しますが、後で見るように、asyncioモジュールには、基本的なプロトコルがそれぞれのトランスポートを備えたようなものとして用意されているので、基本的にはトランスポートを直接触るようなことはないかと思います。BaseTransport:基底トランスポートReadTransport:読み込み専用WriteTransport:書き込み専用DatagramTransport:UDPに対応BaseSubprocessTransport:サブプロセス通信に対応プロトコルasyncioモジュールには、そんな大がかりなプロトコルはなく、それぞれ異なるトランスポートをもつプロトコルとしてサブクラスが発展しているだけです(とはいえ、これらのおかげでトランスポートを直接触らずに済みますから、十分ありがたいものです)。BaseProtocol:基底プロトコルProtocol:TCPやSSLトランスポートを用いたプロトコルDatagramProtocol:UDPトランスポートを用いたプロトコルSubprocessProtocol:サブプロセストランスポートを用いたプロトコル具体的にどういった様式をデータ単位として扱っていくかということについては、独自にプロトコルを組んでいくことになります。aiohttpモジュールがあります)。使うであろう、Protocolクラスについて見ていきます。さあ、ソケットの一生を書き込みましょう:BaseProtocol.connection_made(transport):コネクション作成時に呼び出しBaseProtocol.connection_lost(exc):コネクションが失われた、閉じられたときに呼び出しProtocol.data_received(data):データを受信したときに呼ばれる。Protocol.eof_received():相手が送信するデータがないことを伝えてきたときに呼ばれる。その他のプロトコルについてはドキュメントを見てください(プロトコル)。コネクションの作成コネクションは用いるソケットタイプやアドレスファミリーによって異なりますが、基本的には:loop.create_connection():SOCK_STREAM(TCP)を用いる通信loop.create_datagram_endpoint():SOCK_DGRAM(UDP)を用いる通信を使います。このメソッドはコルーチン関数です。プロトコル)のタプルが返ります。待ちうけコネクション(サーバー)には次のメソッドを使います。引数は上のメソッドと同じです:loop.create_serverこのメソッドもご期待通り、コルーチン関数です。得られるコルーチンは接続が成功するとサーバーオブジェクトを返します。サーバーオブジェクトには次のことができます:server.close():非同期に、待機中のソケットを閉じ、サーバーを停止するserver.wait_closed():close()が完了するまで待機する(もちろん非同期に!)sockets:サーバーが待機するソケットのリストドキュメントが気になるなら、ここに置いておくのでご自由に:http://docs.python.jp/3/library/asyncio-eventloop.html#server流れとしては、プロトコルを渡してコネクションをつくり、あとはプロトコルに書いた通りにソケットが人生(ソケット生)を送るだけです。簡単なやりとり(たとえば、echo)なら、プロトコルの基本メソッドだけで完結すると思います。「直接は触らない」って言ったように、私たちはプロトコル手袋をつけた状態でしかトランスポートを触ることはないはずです。プロトコルのconnection_madeはトランスポートを引数として渡されて呼ばれるので、もしそのソケットでデータを送るようなことがあるなら、そのときにトランスポートに首輪でもはめておいてください。えっと、プロトコルインスタンスの属性にでもしておいてくださいってことです。プロトコルオブジェクトに送信用のメソッドを定義しておくのはいい手かもしれません。渡されたデータに何か小細工してから相手に送りつけたり、その荷物にドラッグが仕込まれてないかを確認したりしたい場合はそうするべきです(ですが、もっと良い方法があって、それはすぐ後で出会うことになります。プロトコルの典型的な実装などについては、標準モジュールのブロッキングなプロトコルを参考にするといいかもしれません(インターネットプロトコル)。8888)loop.run_until_complete(coro)loop.run_forever()loop.close()これが、あるソケットの数奇な人生の記録です。相手に送りつけ、受け取ったデータを単に出力します。なんて退屈な人生なんだ......。接続が切れると、彼はループを止め、やがてプログラムも終了します。ループを2回に分けて回していることに注意してください。いうのは今までに見ませんでしたね。loop.run_until_completeは、渡されたFutureオブジェクトに例外が設定されたときはその例外をあげます。2回に分けて実行することで、万が一接続がうまく行かなかったときにループがそのまま永遠に走り続けるというようなことを防止できるわけですね!続いて、サーバー側も見てみましょう。serverserver.close()loop.run_until_complete(server.wait_closed())loop.close()本当に単純なエコーサーバーです。接続すると、接続した相手の名前をプリントします。相手からデータがきたら、それをプリントし、トランスポートを使ってそのまま相手に送り返します(エコーだからね!)。そのあと、トランスポートを閉じ、これ以上このコネクションでやり取りをしない旨を伝えます。あがるまでは動作しつづけます。新たにクライアントが接続してきたら、さっきと同じことを繰り返してまた接続を切ります。よってループが止まれば、サーバーに閉じるよう命じ、ちゃんと閉じるまでループを回してから、ループも閉じます。それは困りました...。Windowsだと、何も仕事をしていない待機中のイベントループにシグナルが上手く伝わらないようです。押すと上手く反応してくれるので、応急処置としては、絶対に終わらない仕事を与えておく方法があります。応急処置として、ですけど。ソケットによる他のプロセスとのデータのやり取りはもうバッチリなはずです。確かに本当にI/Oのまわりの装備は整ってきましたが、I/Oとコッチ側のやりとりのことを忘れていました。トランスポート/プロトコルは確かにソケットを繋いで、いい感じに相手からデータを受け取ってくれます。私たちがそのデータを受け取ったのは、そのデータを使って何かしたかったからにちがいありません。加えて、トランスポートは相手方にバイナリデータを送ってくれますが、トランスポートに渡すために、毎回自分の手で(より抽象的な)データをプロトコルが規定する様式のバイナリデータに変換するのも面倒です。ストリームはまさに配達業者と私たちを結ぶ窓口のような役割を果たしてくれます。私たちはStreamReader窓口で「荷物ください」と催促し、StreamWriter窓口に行って「この荷物をお願いします」とお願いすればいいのです。各窓口は特定の配達業者と提携していて、私たちの荷物に適切な処置をした上で、その業者に荷物を流してくれます。有り体に言えば、トランスポート/プロトコルよりも高水準なI/Oシステムです。トランスポート/プロトコルとストリームの依存関係ストリームはトランスポート/プロトコルの上になりたつものなので、そこには依存関係があります。簡単にいえば、個々のオブジェクトを作る際の引数に何を渡すのかという関係です。モジュールの構造の関係か、論理上の階層(ストリームがトランスポート/プロトコルより上という階層)と少しだけ変わっています:StreamReader:「最下層」のオブジェクトです。StreamReaderProtocol:ストリーム用にカスタムされたプロトコルで、引数にStreamReaderを取ります。StreamWriter:「最上層」のオブジェクトです。トランスポート、プロトコル、リーダーの3つを引数にとります。分かるとは思いますが、必要な読み書きストリームをつくる際にはStreamReaderオブジェクトを手に入れるそれを使ってStreamReaderProtocolオブジェクトを手に入れるそれら(+トランスポート)を使ってStreamWriterを手に入れるという流れになります。いえ、StreamReaderだけを作っても特にできることは何もないし、まともに使いたいのであればStreamReaderProtocolは必須です。基本的に、ストリームを使うときはStreamWriterまで作るはずなので、なんであれ、トランスポート/プロトコルは必須です。StreamReaderProtocolまずは「受信窓口」から始めましょう。StreamReaderはその内部に倉庫(バッファ(bytearray))をもっていて、StreamReaderProtocolは自分が受け取ったデータをどんどんそこに放り込んでいきます。私たちはStreamReaderのインターフェースを通して倉庫に保管してあるデータを受け取れます。read():引数にバイト数を渡せる。デフォルトではEOFまで読み込むreadexactly():引数にバイト数を渡して、厳密にそれだけ読み込むreadline():改行がくるまで読み取る。私たちとStreamReaderを繋ぐのは上で見たread系のメソッドです。配達業者側、つまりトランスポート/プロトコルとはfeed系のメソッドで荷物を受け付けます:feed_data()feed_eof()これらのメソッドを私たちが直接触ることはないでしょう。StreamReaderProtocolやStreamWriterが内部でこれらのメソッドを良しなに使っているので、これらのサブクラスとして実装すればいいだけです。StreamReaderProtocolはインスタンスをつくる際に、第二引数としてclient_connected_cbにコールバック(関数でもコルーチンでも)を渡すことができます。このコールバックは、接続したときに呼ばれるもので、connection_madeメソッドと似た役割をもちます。ほとんどトランスポートのラッパです;write(data)やwritelines(list_of_data)、write_eof()メソッドをインターフェースとしてもっており、同名のトランスポートのメソッドを呼び出します。十分な場合は、open_connectionが使えます。これはcreate_connectionのラッパーです。writer案の定、まずリーダーを手に入れ、それを使ってプロトコルを手に入れます。トランスポートを手に入れます。最後に、その3つをすべて使って、ライターを手に入れ、リーダーとライターを返します。**kwds))まあ、やってることは大したことないですよね。これでだいたい終わりasyncioモジュールの根幹であるイベントループからはじまり、Futureオブジェクト、コルーチン、トランスポート/プロトコル、ストリームとみてきました。asyncioモジュールの大体の要素は見てきたので、モジュールのドキュメントの大半は見たことあるものになってるかと思います。サブプロセスだけはやってないですが、ここまで理解していればそんな大したことないでしょう(憶測)。asyncio!あっ、run_in_executor書いてない・・・・・・*1:実際、Python3.4以前ではコルーチンはジェネレータの形で書いていましたから、当然といえば当然ですね。*2:具体的にどのくらい頻繁かというと、新しい仕事を始める前に毎回チェックしているようです。co'eはてなブログをはじめよう!waraby0gingerさんは、はてなブログを使っています。ブログを報告するスターをつけました引用をストックしましたストック一覧を見る閉じる引用するにはまずログインしてくださいログイン閉じる引用をストックできませんでした。再度お試しください閉じる限定公開記事のため引用できません。