原文はここからダウンロード可能です: http://everburning.com/news/eventmachine-introductions/
(翻訳の公開と画像の利用は本人より許諾済みです)
翻訳・内容の間違い等があればブログコメントやTwitterなどで遠慮無くご指摘ください。
EventMachine入門
Introduction
うん、これから何を学ぶことになるのか、この導入のくだりがスタート地点として役に立つと思う。EventMachine とは何だろう。そしてそれは私たちのために何をしてくれるのだろう。さて、最初の部分は簡単だね。EventMachine は Reactor パターン(*1)の高性能な実装さ。
すげえ、いや、ちょっと待て、Reactor パターンって何だ? Wikipedia によると:
Reactor デザインパターンとは、一つ以上の入力による、サービスハンドラへ送られた同時並行なリクエストを扱うための並列プログラミングパターンです。サービスハンドラは入ってくるリクエスト群をより分け、関連するリクエストハンドラに同期的に送りつけます。
基本的に、EventMachine(EM) はソケットの受信待機、ネットワーク接続の確立、タイマー処理、そしていくつかの並列プリミティブを含んだ低レベルなもの全てを扱う。EM は高性能なAPIサーバ/サービスを作るのに必要な多くの中核機能を提供する。また EventMachine は Dan Kegel 他による C10K 問題(*2)の教訓を考慮に入れている。
ネットワーク処理能力と共に、EM は長時間かかるタスクをバックグラウンドなスレッドに回すことができるスレッドプールを提供する。これはアプリケーションを素早い・応答性の良いものにしてくれる。みんな応答性は好きだよね? もちろんスレッドプールを使うことに関してはトレードオフがあって、ほとんどは Ruby1.8 のグリーンスレッド(OS ではなく VM がスケジュールするスレッド)の実装に関係するんだ。
(訳注: Ruby1.9ではネイティブスレッドになりましたが、Global Interpreter Lock により完全に並列実行されるわけではありません。ただしグリーンスレッドよりもスレッド切り替えコストは軽減されています)
最初に、簡単な例を見てみようか。EM におけるHello World、エコーサーバを例として使ってみよう。
緑色のテキストはサーバからの応答だから注意してね。
require 'eventmachine' class Echo < EM::Connection def receive_data(data) send_data(data) end end EM.run do EM.start_server("0.0.0.0", 10000, Echo) end
Rei:~ dj2$ telnet localhost 10000 Trying 127.0.0.1... Connected to localhost. Escape character is '^]'. helo helo goodbye cruel world goodbye cruel world
詳細は後に見ていくけど大雑把に言うと、ポート10000番上で開始されたサーバは接続を確立したときに、用意しておいた Echo クラスを使っている。ポートにデータが届いた時、EM は receive_data メソッドを実行し、send_data を呼ぶことによって、受け取ったデータをクライアントにエコーし返す。
本質的には、Echo クラスのインスタンスはファイルディスクリプタに結びつけられている。ファイルディスクリプタに動きがあればいつでも、君のクラスのインスタンスはそのアクションをハンドルするために呼ばれるのだ。これがほとんどの EventMachine プログラミングの基礎であり、reactor はファイルディスクリプタを監視し、君のクラスのインスタンスにあるコールバックを実行する。
注目すべき興味深いことは、我々の Echo クラスにはどんなネットワーク原理の概念もないってことだ。実装した Echo#receive_data にはパケットやヘッダの概念はないけれど、ネットワークのデータを取り出すことができる。
Getting Started
導入はやりのけたので、EventMachine の楽園へのユカイな道のりを始められる。君が邁進するのに使えるいくつかの基本的タスクとコマンドがあるから、そこを原点として始めようか。
最初に、最も簡単で最もばかげた EM アプリケーションを作ってみよう。
require 'eventmachine' EventMachine::run
ちっとも刺激的じゃない例だが、良い出発点だ。注意が2、3ある。まず最初に、EventMachine を利用するには、eventmachine を require しなくちゃならない。2番目に、抜け目ない人ならお気づきだろうが、最初の例では EM. を使っていたが今回は EventMachine:: を使った。これは EM:: でも EventMachine. でもいい。どれも同等の書き方だ。個人的には EM. が短くて簡潔なので好きだ。最後に、もしこの例を実行させたなら、決して終了しなかったことに気づくだろう。EM#run を呼ぶと、EventMachine reactor は開始され、止まれと言われるまではハッピーに走り続ける。そして実際に今は止まれと言わなかった。
どうやってこの野獣ちゃんを止めたらいい? とっても簡単。
require 'eventmachine'
EM.run do
EM.add_timer(1) { EM.stop }
end
こっちでは EM#run にブロックを渡している。ブロックの中身は reactor が初期化されたあと、 reactor が走り始める前に実行される。必要な初期化処理があれば、なんでもこのブロックに入れることができる。この場合は、時が来たら EM#stop を実行してくれるタイマーを作成している。EM#stop の代わりに EM#stop_event_loop を呼ぶこともできる。この二つのメソッド呼び出しはどちらも同じもので、reactor を終了させる。reactor が終了すれば、EM#run ブロックのうしろに書かれたコードが実行される。
Timers
ツアーの次の停留所は EventMachine タイマーだ。タイマーには二つのタイプが存在する。一回限りのタイマーと、周期的なタイマーだ。EventMachine にタイマーを加えるには二つの異なる、だが同等の効果の方法がある。一般的な方法としては、下記で見るように EM#add_timer と EM#add_periodic_timer を使うやり方だ。require 'eventmachine' EM.run do EM.add_timer(5) do puts "BOOM" EM.stop_event_loop end EM.add_periodic_timer(1) do puts "Tick ... " end end
titania:examples dj2$ ruby timer.rb Tick... Tick... Tick... Tick... BOOM
この例では二つのタイマーをシステムに追加する。周期的なタイマーは多くとも1秒に一度実行され、一回限りのタイマーは少なくとも5秒後には実行されるだろう。この(「多くとも」とか「少なくとも」みたいな)言葉づかいは超重要だ。EventMachine はタイマーがいつ実行されるのか保証しない。指定したフレーム時間で実行されるかもしれないだけだ。
以下の同等の例では、メソッドの代わりにクラスを用いている。
require 'eventmachine' EM.run do EM::Timer.new(5) do puts "BOOM" EM.stop end EM::PeriodicTimer.new(1) do puts "Tick ..." end end
機能面ではこれらの二つの例は同一だ。私は最初に見た方、EM#add_timer と EM#add_periodic_timer の方をより多く使う。
(訳注:以下の問題は現在の EventMachine(0.12.10) では存在しません。
EM#add_periodic_timer の返り値は EM::PeriodicTimer オブジェクトであり、
EM#cancel_timer にそれを渡すと中で EM::PeriodicTimer#cancel メソッドを呼んでくれます)
メソッドではなくクラスを使用しなければならない場合も一つだけある。それは周期的なタイマーをキャンセルする場合だ。 EM#add_timer の返り値のシグネチャを EM#cancel_timer に渡して実行すると、一回限りのタイマーはキャンセル出来る。周期的なタイマーでは、タイマーが再スケジュールされる度に、毎回新しいシグネチャを受け取ってしまうという問題があるんだ。そして、シグネチャを知らなければタイマーをキャンセルことができない。
もし周期的なタイマーをキャンセルさせる必要があるなら、EM::PeriodicTimer を使う必要がある。これは EM::PeriodicTimer#cancel というメソッドを提供しているんだが、もうおわかりの通り、周期的なタイマーをキャンセルできる。
require 'eventmachine' EM.run do p = EM::PeriodicTimer.new(1) do puts "Tick ..." end EM::Timer.new(5) do puts "BOOM" p.cancel end EM::Timer.new(8) do puts "The googles, they do nothing" EM.stop end end
titania:examples dj2$ ruby timer_cancel.rb Tick... Tick... Tick... Tick... BOOM The googles, they do nothing
Deferring and Delaying Work
Reactor パターンの定義を思い返してみると、 reactor はシングルスレッドだったのに気づくだろう。EventMachine もこのシングルスレッドなアプローチの reactor に当てはまる。reactor 自身はシングルスレッドであり、reactor で扱う EM のメソッドはスレッドセーフではない。これは2つの結果になる。まず第一に、おそらく私たちは長い時間かかるコードを持つってことだ。データベースクエリや、リモートHTTPリクエスト、その他もろもろ。効率のためには、これらのコードをバックグラウンドのスレッドにやらせる必要がある。第二に、コードをバックグラウンドのスレッドに追いやったら、それを動かしてくれるように reactor に命令できるようにする必要もある。
ここで EM#defer と EM#next_tick のおでましだ。
EM#defer を使うと EventMachine のスレッドプールへ、実行するコードブロックを一つのスレッドとしてスケジュールすることができる。このスレッドプールは(デフォルトでは)最大で20個までのスレッドを提供する。やべぇ、俺らのコードをバックグラウンドで走らせたら、その結果をどうやってクライアントに伝えるんだ? もし必要なら、EM#defer は第二のパラメータをとる。callback だ。このコールバックはメイン reactor スレッド上で実行され、スレッドプールに回された操作の返り値を提供する。我々は、クライアントとコミュニケーションするために、このコールバックを使うことができる。
EM#defer には悪い面もあって、Ruby1.8 のスレッドの実装で動かさなきゃいけないということだ。 Ruby には本当のOSスレッドがない。グリーンスレッドと呼ばれるものならある。(一つのRubyプロセスの)全てをその上で動かすOSスレッドが一つあり、Ruby はそのOSレベルスレッド上で自分のスレッドを作成する。これは、 Ruby が全てのスレッドスケジューリングを扱っていることを意味していて、そしていずれにしてもあなたのプロセスがブロックするだろうOSレベルスレッドをつかむ。だから、あなたが延期処理を行うどのスレッドの中でも、Ruby をブロックしないと確信できるように注意しなくちゃならない。じゃないと全体をブロックしてしまう。
(訳注:先述の通り、Ruby1.9ではネイティブスレッドになっています。1.9ではOSがスレッドスケジューリングを扱いますが、GILにより一度に走るスレッドは一つだけです。よって注意すべき点は変わりません)
EM#next_tick を使うと、次の reactor ループイテレーションで発生するように、実行するコードブロックをスケジュールできる。実行はメイン reactor スレッドで起こる。事実上この遅延は瞬時に起っていると考えることができて、これは本来コードが異なるスレッドで動作することを意図してスケジュールしている。EM はスレッドセーフではないから、スレッドプールのとあるスレッドから EM#defer を使おうとすると、深刻な問題に陥ったりクラッシュする可能性がある。
例えば EM#defer なスレッドで何かを動かしていて、EM::HttpClient を使って新しい接続を確立する必要があるとしたら、それはメインスレッド上で行う必要がある。となると接続の作成は、EM#next_tick へ渡すコードブロックの中ですればいいわけだ。
EM#next_tick
EM#next_tick を使うことは、EM#add_timer を動かすのにとてもよく似ている: EM#next_tick に渡した特定のコードブロックは、reactor ループの次のイテレーションで実行される。require 'eventmachine'
EM.run do
EM.add_periodic_timer(1) do
puts "Hai"
end
EM.add_timer(5) do
EM.next_tick do
EM.stop_event_loop
end
end
end
titania:examples dj2$ ruby next_tick.rb Hai Hai Hai Hai
EM#next_tick でスケジュールされたものは、メインスレッドで同期的に実行される。EM#next_tick ブロック内のどんな長い時間かかるタスクも、それの実行を終えるまでプログラム全体をブロックしてしまう。通常、これはよくないことだ。
EM#defer
同様に、EM#defer を使うことは、その実行がバックグラウンドスレッドで終了した後に、メインスレッドで実行されるコールバックを提供する能力を与えてくれる。また、コールバック関数は渡さなくてもよい。require 'eventmachine'
require 'thread'
EM.run do
EM.add_timer(2) do
puts "Main #{Thread.current}"
EM.stop_event_loop
end
EM.defer do
puts "Defer #{Thread.current}"
end
end
titania:examples dj2$ ruby defer.rb Defer #<Thread:0x5637f4> Main #<Thread:0x35700>
EM#defer に渡したブロックは、バックグラウンドスレッドで実行が開始され、ゴキゲンに走り続ける。注意だが、あなたのコードはこのスレッドを永遠に占有しないようにしなければならない。EventMachine はこの状態を検知しないので、スレッドが無期限に実行されてしまう。スレッドプールのサイズは決っているため、そうやってスレッドを失ったら、その分取り戻せなくなる。
また、コードが走り終わったあとにコールバック関数を実行するという EM#defer の機能を利用できる。コールバック関数に仮引数を指定していたならば、コードの返り値がそこに渡されてから実行される。
require 'eventmachine'
EM.run do
op = proc do
2+2
end
callback = proc do |count|
puts "2 + 2 == #{count}"
EM.stop
end
EM.defer(op, callback)
end
Rei:EventMachine dj2$ ruby defer_callback.rb 2 + 2 == 4
ここでは二つの数字を足す proc オブジェクトを作成している。この合計の結果は、EM#defer スレッドでの実行から返ってきて、コールバックへと渡される。そしてコールバックはメイン reactor スレッド上で実行される。このコールバックは結果を出力している。
Lightweight Concurrency
EventMachine は軽量な並行処理(Lightweight Concurrency)(*3)を扱うための2つのメカニズムを内蔵している。spawned process と deferrable だ。注意して欲しいのは、spawned process なんて呼ばれてはいるものの、OSのプロセスとは違うってことだ。この名前は Erlang から来ているのだが少し紛らわしい。それら二つのメカニズムにひそむ主要なアイデアは、CPU やメモリにとってより軽く、そして標準のRubyスレッドであるってことだ。軽量の並列動作における多くの働きは、あなたのアプリケーション側でハンドルされる必要がある。deferrable と spawned process によるコードはあなたのアプリケーションから実行要求が来るまで、実行されない。
これらのメカニズムがどう動くのか、見て行こう。
EM::Deferrable
EM::Deferrable(*4) を見ていくところから始めよう。あなたのクラスに EM::Defferable をミックスインしたとき、そのクラスのインスタンスには callback と errback 関連の能力が提供される。あなたは実行される callback と errback をいくらでも定義できる。callback と errback は、インスタンスに追加された順番で実行される。callback と errback を発動させるためには、インスタンスオブジェクトの #set_deferred_status を呼び出す。そのメソッドには :succeeded か :failed を渡すことができて、:succeeded なら callback が、:failed なら errback が発動する。これらのブロックは、メインスレッド上ですぐさま実行されるだろう。また、deferrable オブジェクトに状態をセットするために、#succeed と #fail というシンタックスシュガーも用意されている。
一旦オブジェクトの状態が指定されると、その後で追加されるどんな callback や errback も、追加された途端にその状態に応じてただちに実行される。
これが実際にどう動くか見てみよう。
require 'eventmachine' class MyDeferrable include EM::Deferrable def go(str) puts "Go #{str} go" end end EM.run do df = MyDeferrable.new df.callback do |x| df.go(x) EM.stop end EM.add_timer(1) do df.set_deferred_status :succeeded, "SpeedRacer" end end
titania:EventMachine dj2$ ruby deferrable.rb Go SpeedRacer go
ここで何が起っているか注意深く見てみよう。最初に EM::Deferrable をクラスに include している。これで deferrable するのに必要なメソッドがミックスインされる。
普通に MyDeferrable クラスを作成して、そのインスタンスで #callback と #errback を呼んでいる。callback と errback のブロックには、仮引数をいくらでも指定できる。処理が成功したか失敗したかが決まった時、インスタンスの #set_deferred_status を実行している。この場合では、:succeeded を渡し、callback には一つの仮引数を指定して定義したから、"SpeedRacer" が callback に渡される。
デフォルトでは、全ての callback が同じ引数で実行される。callback 内で #set_deferred_status をもう一度呼び、異なる引数を渡すことで、それ以降の callback には違う引数を渡すことも可能だ。これは deferrable のドキュメントでより詳しく解説される。
deferrable なオブジェクトを使うために、わざわざクラス全体を実装する必要がない場合もあるだろう。そういう場合のため、 EventMachine は EM::DefaultDeferrable というクラスを提供している。カスタムクラスでやる代わりに、EM::DefaultDeferrable.new をただするだけで、 EM::Deferrable をミックスインしたものと全く同じ動作をしてくれる。
EM::SpawnedProcess
EventMachine の spwened process は、Erlang のプロセスにインスパイアされている。OSのプロセスではないのにプロセスというネーミングには少し混乱させられるが。spawned process が持つアイデアは、プロセスを作成できて、それにコードを付加することができるということだ。未来のいつかの時点で、spawn されたオブジェクトの #notify メソッドを呼び出すことで、付加したブロックを実行することができる。deferrable と異なりコードブロックはすぐ実行されないが、#notify 呼び出しがあったならいずれは実行される。
require 'eventmachine' EM.run do s = EM.spawn do |val| puts "Received #{val}" end EM.add_timer(1) do s.notify "hello" end EM.add_periodic_timer(1) do puts "Periodic" end EM.add_timer(3) do EM.stop end end
Rei:EventMachine dj2$ ruby spawn.rb Periodic Received hello Periodic
Network Fun
さあて、イイモノの紹介に入ろう。EM は、ネットワークプログラミングを扱うように設計されている。どんなプロトコルだろうと、一連のベースプロトコル実装だろうが、その能力はAPI開発者やサーバ開発者に楽をさせる。Servers
サーバーサイドから始めることにしよう。クライアント側のコードとはとてもよく似ているけど、それは次のセクションでやる。イントロダクションで使ったエコーサーバの例に戻ってみよう。
require 'eventmachine' class Echo < EM::Connection def receive_data(data) send_data(data) end end EM.run do EM.start_server("0.0.0.0", 10000, Echo) end
Rei:~ dj2$ telnet localhost 10000 Trying 127.0.0.1... Connected to localhost. Escape character is '^]'. helo helo goodbye cruel world goodbye cruel world
君にも分かるとおり、EM::Connection クラスを継承して Echo クラスを作成している。そして EM#start_server メソッドを使い、さっき定義した Echo クラスを用いて、全てのインターフェースを監視する、ポート10000番上のサーバを作っている。
おもしろいことに、これと同じコードを書く方法がなんと二つ以上存在している。どれを使うかは好きにしていい。
モジュールを使うこともできる:
require 'eventmachine'
module Echo
def receive_data(data)
send_data(data)
end
end
EM.run do
EM.start_server("0.0.0.0", 10000, Echo)
end
またはブロックでも:
require 'eventmachine' EM.run do EM.start_server("0.0.0.0", 10000) do |srv| def srv.receive_data(data) send_data(data) end end end
3つの全ての例で、新しいコネクションが確立したとき、新しい匿名のクラスがあなたのコードを取り込んで作成される。これは大事なことで、コネクション毎にあなたのクラスの新しいインスタンスを持つことになるってことだ。インスタンスは次の接続の時には存在しないので、コネクション間でインスタンス内には何も保存することはできない。
サーバとして機能させるためには #receive_data(data) という一つのメソッドを実装する必要がある。もし #receive_data を実装しなかったら、"............>>>6" みたいなものがコンソールに吐出されるのを見るハメになる。もし君が私に似ているなら、それがどこから来ているのか調べるために30分費やすことになるだろう。
#receive_data のようにクライアントとの接続がある間に呼び出される、いくつかのメソッドが他にもある。
post_init | インスタンスの初期化時、コネクションが完全に確立する前に呼ばれる。 |
connection_completed | コネクションが完全に確立した後に呼ばれる。 |
receive_data(data) | クライアントからデータを受け取った時に呼ばれる。データはチャンクで受け取る。チャンクの組み立てはあなたの責任だ。 |
unbind | クライアントとの接続が完全に切れたときに呼ばれる。 |
私たちの例では、クライアントに返送するために #send_data(data) を使った。巨大なデータファイルを送りたいなら、巨大なデータのチャンクを送り出すのに便利なメソッドである #send_file_data(filename) を使うこともできる。
最後に、ここには示されていないが #close_connection と #close_connection_after_writing という二つの便利なメソッドがある。これら2つのメソッドはとても似た操作を行う。両方ともクライアントが接続を閉じた時にシグナルを飛ばす。#close_connection_after_writing は接続が閉じられる前に #send_data で送った全てのデータを確実にクライアントに送るという点で異なる。
以前に言及したように、接続と接続の間で君のクラスのインスタンスはどんな情報も共有できない。幸運にも、いや、設計されたことだろうが、EventMachine はこれを処理するメカニズムを提供する。
require 'eventmachine' class Pass < EM::Connection attr_accessor :a, :b def receive_data(data) send_data "#{@a} #{data.chomp} #{b}" end end EM.run do EM.start_server("127.0.0.1", 10000, Pass) do |conn| conn.a = "Goodbye" conn.b = "world" end end
titania:~ dj2$ telnet localhost 10000
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
mostly cruel
Goodbye mostly cruel world
EM#start_server へ渡されたブロックの中で、Pass インスタンスは自身が初期化された後、クライアントからデータを受け取る前に、EventMachine から値が渡されている。このどんな状態でもセットされたインスタンスを、必要ならば使うことができる。
Clients
ひとたびサーバを起動したならば、クライアントでそれに接続するのが有益だろう。幸い、サーバの動作を知った後なので、魔法を起こすのに何が必要か大部分は知っているよね。require 'eventmachine'
class Connector < EM::Connection
def post_init
puts "Getting /"
send_data "GET / HTTP/1.1\r\nHost: MagicBob\r\n\r\n"
end
def receive_data(data)
puts "Received #{data.length} bytes"
end
end
EM.run do
EM.connect("www.postrank.com", 80, Connector)
end
titania:EventMachine dj2$ ruby connect.rb Getting / Received 1448 bytes Received 1448 bytes Received 1448 bytes Received 1448 bytes Received 2896 bytes Received 1448 bytes Received 1448 bytes Received 935 bytes
EM#connect によりコネクションが作成される。このコード例は、上でサーバを作成したときに述べられた考えに従っている。コールバックメソッドは同じ名前を持ち、サーバにデータを送るメソッドも、サーバがクライアントにデータを送るときと同じ #send_data だ。
Conclusion
EventMachine の連続公演はこれにておしまい。私たちは、物事の成し遂げ方と、EM#runを使ってそれが走るのを見た。一回限りのタイマーと、周期的なタイマーを作成すること。Deferring と next_tick のコードブロック。deferrable と spawned process をクライアントとサーバで作成すること。
たぶん、全てを見たあとでは、あなたは Eventmachine がどう機能するか、そしてあなたのアプリケーションでどう使えばいいのか、より理解が深まったことだろう。
何か質問やコメントがあるならば、遠慮なく私に連絡を。: dan(at-mark)aiderss.com
Resources
http://rubyeventmachine.com/
http://github.com/eventmachine/eventmachine/tree/master
http://groups.google.com/group/eventmachine/topics
http://eventmachine.rubyforge.org/
http://www.igvita.com/2008/05/27/ruby-eventmachine-the-speed-demon/
http://20bits.com/articles/an-eventmachine-tutorial/
http://nutrun.com/weblog/distributed-programming-with-jabber-and-eventmachine/
http://www.infoq.com/news/2008/06/eventmachine
http://everburning.com/news/playing-with-eventmachine/
http://blog.nominet.org.uk/tech/2007/10/12/dnsruby-and-eventmachine/
http://devver.net/blog/2008/10/sending-files-with-eventmachine/
http://en.oreilly.com/rails2008/public/schedule/detail/1820
http://nhw.pl/wp/2007/12/07/eventmachine-how-to-get-clients-ip-address
http://simonwex.com/articles/2008/10/22/eventmachine-http-client
http://adrianhosey.blogspot.com/2009/01/eventmachine-tcp-server-module-for-ruby.htm
*1 http://en.wikipedia.org/wiki/Reactor_pattern
*2 http://www.kegel.com/c10k.html
*3 http://rubyeventmachine.com/browser/trunk/docs/LIGHTWEIGHT_CONCURRENCY
*4 http://rubyeventmachine.com/browser/trunk/docs/DEFERRABLES