複数のプロセスで動作するシステムを作成しているとします。 たとえば、バックグラウンドで長時間実行されるタスクを実行するWebサーバーがあります。 または、あるプロセスから別のプロセスへのデータの転送を確実にし、それらを調整する必要があります。 このような状況では、DRbライブラリが必要です。 完全にRubyで記述されており、標準ライブラリに含まれているため、すぐに作業を開始できます。 接続するには、
require 'drb'
書くだけ
require 'drb'
DRbライブラリの長所は、主にRuby言語自体のダイナミズムにあります。
第一に、準備段階で最小限の労力を費やす場合、あるプロセスまたは別のプロセスで、ためらうことなくオブジェクトのある場所で作業します。 ライブラリは、すべての技術的な詳細を完全に隠しています。
第二に、インターフェイスをハードコードする必要はありません。 rubyオブジェクトは、そのインターフェイスを外部に公開できます。この方法では、どちらも
Hash
や
Queue
などの標準クラスのいずれかの機能を使用したり、任意のインターフェイスで独自のクラスを作成したりできます。 さらに、実行中にインターフェイスを直接変更したり、
method_missing
を使用して
method_missing
を処理することもできます。 そしてもちろん、クライアントが署名または動作を変更したメソッドを呼び出さない場合、サーバーインターフェイスの更新はクライアントにまったく影響しません。 したがって、サーバーとクライアントは可能な限り独立しています。
そして最後に、クライアントはサーバーから返されるオブジェクトのクラスを知る必要さえありません。クライアントはそれを使用せずに使用できます。 したがって、サーバーは必要なだけ詳細を非表示にできます。
しかし、もちろん、落とし穴があり、それらはたくさんあります。 幸い、dRubyの理解は簡単ですが、その構造を理解することで、ほとんどの問題を簡単に防ぐことができます。 残念ながら、このライブラリのドキュメントでは多くの点が明確にされていないため、この記事は初心者と既にライブラリを使用したことがある人にとって興味深いものになります。
すべてが機能することを確認するには、2つのirb端末を開きます。 ルビー1.8の違いがどれほど大きいかわからないので、バージョン1.9について議論していることに同意しましょう(特に1.8以降-すぐにサポートが終了します!
条件付きで、これら2つの端末はサーバーとクライアントです。 サーバーは、リクエストを受信するフロントオブジェクトを提供する必要があります。 このオブジェクトには、組み込み型のオブジェクト、特別に作成されたインターフェイスを持つモジュールなど、任意のオブジェクトを指定できます。 次に、クライアントはサーバーに接続し、このオブジェクトと対話します。
たとえば、最初の端末でサーバーを起動し、通常の配列を配置しましょう。
require 'drb' front = [] DRb.start_service('druby://localhost:1234', front) front << 'first' # , - DRb.thread.join
クライアントを接続します。 配列の最初の要素を認識し、別の要素を配列に書き込みます
require 'drb' DRb.start_service remote_obj = DRbObject.new_with_uri('druby://localhost:1234') p remote_obj p remote_obj[0] remote_obj << 'second'
これで、最初の端末から
front[1]
呼び出して、文字列
'second'
ます。 また、別のクライアントを接続し、そこから同じ正面オブジェクトを操作することもできます。
既に
DRb.start_service
に、サーバーは
DRb.start_service
コマンドによって起動されます(レジスタに注意してください!)。 このメソッドは、引数として
'druby://hostname:port'
という形式
'druby://hostname:port'
アドレスとフロントエンドオブジェクトを持つ文字列を取ります。 フロントエンドオブジェクトは、リクエストを受信するオブジェクトです。
サーバーが別のスクリプトで起動したら、スクリプトの最後に
DRb.thread.join
を記述します。 実際、DRbサーバーは別のスレッドで起動され、メインスレッドが完了するとすぐにRubyがプログラムをシャットダウンします。 したがって、メインスレッドがDRbサーバーストリームのクローズを待たない場合、両方のストリームがスケジュールより早く完了し、サーバーはすぐに利用できなくなります。
DRb.Thread.join
メソッドを実行すると、サーバーの電源が切れるまで現在のスレッドがブロックされるという事実に備えてください。
サーバーに接続するには、
DRbObject.new_with_uri
メソッドを呼び出し、サーバーが起動するアドレスを引数として渡す必要があります。 このメソッドは、
remote_obj
プロキシオブジェクトを返します。 プロキシオブジェクトへのリクエスト(メソッド呼び出し)は、リモートサーバー上のオブジェクトに自動的に転送され、呼び出されたメソッドがそこで実行された後、メソッドを呼び出しているクライアントに結果が返されます。 (ただし、すべてのメソッドがサーバーで呼び出されるわけではありません。たとえば、動作によって判断すると、
#class
メソッド
#class
ローカル
#class
実行されます)
クライアントは、
DRb.start_service
コマンドの意味
DRb.start_service
少し後で説明します。
最後に、リモートオブジェクトのメソッドがどのように実行されるかを理解しましょう。 これを行うには、プロキシオブジェクトのメソッドを呼び出すと、メソッドの名前と引数のリストがシリアル化(マーシャリング)され、TCPプロトコルを使用して結果の文字列がサーバーに転送されます。サーバーは呼び出し引数を逆シリアル化し、フロントエンドオブジェクトでメソッドを実行し、結果をシリアル化してクライアントに返します。 すべてがシンプルに見えます。 実際、通常のリモートオブジェクトと同じ方法でリモートオブジェクトを操作し、メソッド、プロキシオブジェクト、およびサーバーのリモート実行のための多くのアクションが非表示になります。
しかし、それほど単純ではありません。 リモートメソッドの呼び出しは高価です。 サーバー上のメソッドが多くの引数メソッドを「プル」することを想像してください。 これは、サーバーとクライアントが計算を行う代わりに、時間の大部分が比較的遅いプロトコルを使用して互いにアクセスすることを意味します(2つのプロセスが同じマシンに配置されていると便利です)。 これを防ぐために、プロセス間の引数と結果は参照ではなく値で渡されます(オブジェクトのマーシャライゼーションはオブジェクトの内部状態のみを格納し、その
object_id
認識しません-したがって、オブジェクトは最初にシリアル化され、その後、逆シリアル化は元のオブジェクトのコピーになります、同じオブジェクトによるものではないため、転送はコピーによって自動的に行われます)。 Rubyでは通常、すべてが参照によって渡され、dRubyでは通常は値によって渡されます。 したがって、
front[0].upcase!
を実行すると サーバーでは、
front[0]
値が変更され、
remote_obj[0].upcase!
を実行すると 、最初の要素を大文字で取得しますが、
remote_obj.[](0)
は最初の要素のコピーであるため、サーバー上の値は変更されません。 この呼び出しは、
front[0].dup.upcase!
と同様と見なすことができます
front[0].dup.upcase!
ただし、引数と結果を参照で渡すような方法でdRubyの動作をいつでも定義できますが、これについては後で詳しく説明します。
今こそ、最初の問題について話し合うときです。 すべてのオブジェクトが整列化されるわけではありません。 たとえば、ProcおよびIOオブジェクト、およびスレッド(スレッドオブジェクト)は、コピーを介してマーシャリングおよび送信できません。 この場合のdRubyは次のように進みます。マーシャライゼーションが機能しなかった場合、オブジェクトは参照渡しされます。
では、オブジェクトはどのように参照渡しされますか? Cを思い出してください。 そこでは、ポインターがこの目的に使用されます。 Rubyでは、ポインターの役割は
object_id
によって実行されます。 オブジェクトを参照渡しするには、
DRbObject
クラスのオブジェクトが
DRbObject
ます。
実際、
DRbObject
は参照渡しのプロキシオブジェクトです。 このクラスのインスタンス
DRbObject.new(my_obj)
に
object_id
オブジェクト
my_obj
object_id
オブジェクトの
my_obj
のサーバーのURIが含まれています。 これにより、メソッド呼び出しをインターセプトして、メソッドが意図されたリモートマシン(または別の端末)上のオブジェクトに渡すことができます。
サーバーをメソッドにしましょう
def front.[](ind) DRbObject.new(super) end
そして、クライアントからコードを実行します。
remote_obj.[0].upcase!
新しいメソッド
#[]
は、最初の要素のコピーではなく、リンクを返しました。そのため、
upcase!
メソッドを実行した後に
upcase!
フロントオブジェクトが変更された場合、これは、たとえばクライアントとサーバーからそれぞれ
puts remote_obj
または
puts front
コマンドを実行することで簡単に確認できます。
しかし、
DRbObject.new
を書く
DRbObject.new
-怠
DRbObject.new
です。 幸いなことに、値ではなく参照によってオブジェクトを渡す別の方法があります。 これを行うには、オブジェクトを非整列化可能にすれば十分です。 これは簡単で、オブジェクトに
DRbUndumped
モジュールを含めるだけです。
my_obj.extend DRbUndumped class Foo; include DRbUndumped; end
これで、オブジェクト
my_obj
と
Foo
クラスのすべてのオブジェクトが参照によって自動的に渡されます(そして
Marshal.dump(my_obj)
は
TypeError 'can\'t dump'
を
TypeError 'can\'t dump'
ます)。
実際に出会った例を挙げましょう。 サーバーはハッシュをフロントオブジェクトとして設定します。この場合、値はチケットです(チケット内からはステートマシンです)。 次に、
remote_obj[ticket_id]
はチケットのコピーを提供します。 ただし、これによりサーバー上のチケットステータスを変更することはできず、ローカルでのみ変更できます。
Ticket
クラスに
DRbUndumped
を取得しましょう。 これで、ハッシュからチケットのコピーではなく、チケットへのリンクが得られます。そして、それに対するアクションは、クライアント上ではなくサーバー上で直接発生します。
そして今、約束を思い出して、クライアントで
DRb.start_service
を呼び出す必要がある理由を教えてください。 最初の例のように、サーバー上のフロントオブジェクトによって配列が示されることを想像してください。
ここで、クライアントが
remote_obj.map{|x| x.upcase}
メソッドを呼び出します
remote_obj.map{|x| x.upcase}
remote_obj.map{|x| x.upcase}
実際、ブロック引数を持つmapメソッドは、フロントオブジェクトで呼び出されます。 そして、私たちが思い出すように、私たちはマーシャリングできません。 したがって、このブロック引数は参照渡しされます。 サーバー上の
map
メソッドは
yield
でアクセスします。これは、クライアントがサーバーであることを意味します! ただし、クライアントは時々サーバーである必要があるため、
start_service
メソッドを使用してDRbサーバーも起動する必要があります。 このサーバーのURIを指定する必要はありません。 内部からどのように機能するか、私にはわかりませんが、機能します。 そして、すでにお気付きのように、クライアントとサーバーの違いは見た目よりも小さいです。
新しい迷惑につまずくリスクがあります。 メソッドが、メソッドで直接生成されたオブジェクトへのリンク(コピーではなく)を返したとします。 サーバーがこのオブジェクトを別の場所に個別に保存しなかった場合(たとえば、特別なハッシュに入れなかった場合)、サーバーへのリンクはありません。 リモートマシン上のクライアントは持っていますが、サーバーは持っていません! したがって、遅かれ早かれ、このオブジェクトの
DRbObject
ようなリンクが「不良」になり、どこにもリンクされないことを意味します。 このオブジェクトのメソッドにアクセスしようとすると、エラーが発生します。
したがって、少なくともサーバーによって使用されるまで、返されたオブジェクトへのリンクをサーバーが保管するように注意する必要があります。 これにはいくつかの解決策があります。
1)参照によって渡されたすべての返されたオブジェクトを配列に保存します-リンクが使用されるため、ガベージコレクターはそれらを収集しません。
2)クライアントにブロックへのリンクを送信します。 例:
このコードの代わりに:
Ticket.send :include, DRbUndumped def front.get_ticket Ticket.new end foo = remote_obj.get_ticket foo.start foo.closed? # foo . , start .
次のように書かなければなりません:
Ticket.send :include, DRbUndumped def front.get_ticket object_to_reference = Ticket.new yield object_to_reference end remote_obj.get_ticket do |foo| foo.start foo.closed? end
サーバー上の有効なローカル変数は、ガベージコレクターによって収集できません。 したがって、ブロック内では、リンクが機能することが保証されています。
3)この本は別の方法を説明しています-オブジェクトの
object_id
受け取る段階でリンクを作成するプロセスに
object_id
この時点でガベージコレクションプロセスを何らかの方法で遅らせる必要があります。 ハッシュに要素を自動的に追加し、オブジェクトを永久に保存することができます(推測できるように、メモリは遅かれ早かれ使い果たされるでしょう)、オブジェクトへのリンクを保存して手動でクリアできます。このハッシュは数分ごとにクリアできます。
最後のメソッドは、次のようにして実装できます。
require 'drb/timeridconv' DRb.install_id_conv(DRb::TimerIdConv.new)
サーバーを起動する前に。 詳細については、本の第11章-ガベージコレクションの処理を参照してください。 私には興味深いようです。おそらくそれを読んだ後、ガベージコレクションプロセスの操作を使用する新しい方法が見つかるでしょう。 それでも、実際には2番目の方法を使用して、ブロックへのリンクを与える方が良いと思います。 より信頼性が高く理解しやすい。
おそらく最後の瞬間を照らすために残っています。
Foo
オブジェクトをリンクとして渡すと仮定します。 クライアントは
Foo
クラスについては知りませんが、それでもオブジェクトでの動作を妨げることはありません。 基本的に、クライアントは
DRbObject
クラスのオブジェクトを操作します。 すべてがいつも通りです。
ここで、リンクではなくコピーを送信していると想像してください。 サーバーでのシリアル化により、オブジェクトの状態とそのクラスの名前が保存されました。 クライアントは文字列を受信し、それをデシリアライズしようとしています。 もちろん、クライアントは存在しないクラス
Foo
オブジェクトを作成できないため、これは彼には機能しません。 次に、逆シリアル化
DRb::DRbUnknown
、
DRb::DRbUnknown
型のオブジェクトが返され、マーシャリングされたオブジェクトと共にバッファーが格納されます。 このオブジェクトは(たとえば、タスクキューに)渡すことができます。 また、クラスの名前を確認し、適切なライブラリをクラスにロードして、
reload
メソッドを呼び出すこともできます。その後、逆シリアル化が再度試行されます。 それは
いいえ、まだこれが最後の瞬間ではありません。 同期については書かないことを約束しましたが、それでもいくつか言葉を述べます。
分散プログラミングの場合、アクションの同期と操作の原子性は重要な概念です。 サーバーは別のスレッドで起動します。 サーバーへのリクエストごとに、このリクエストが処理される個別のストリームが自動的に作成されます。 したがって、異なるスレッドが同じ情報に同時にアクセスすることを禁止する必要があります。 そのため、分散システムおよび並列システムをプログラミングする場合は、次を使用します。
1)構築
lock = Mutex.new; lock.synchronize{ do_smth }
lock = Mutex.new; lock.synchronize{ do_smth }
2)標準
MonitorMixin
ライブラリのモジュール
3)標準ライブラリクラスの
Queue
、
SizedQueue
DRbを使用して頑張ってください! 破壊的な方法を使用しているにもかかわらず、オブジェクトが変更されない理由、ブロックを受け入れる方法をクライアントで機能させる方法、受信したリンクが機能し動作した理由を理解しようとして、誰かが長い時間を費やさないように願っています。
ただし、この本では、特にRindaライブラリとその同等物について、さらに多くを見つけることができます。