
分散オブジェクト環境を学ぶ
連載:HORBと遊ぼう(6)
やさしく学ぶ並列プログラミング
萩本順三
HORB Openマネージャ
株式会社豆蔵
2001/11/27
(1)非同期メソッドを理解する |
前回は、コンフィグレーションファイルを使ってHORBサーバの起動形態について説明しました。今回は、分散システムのだいご味ともいえるパラレルプログラミング(並列プログラミング)についてお話ししましょう。というと何か難しそうに思えますが、HORBを使うとこのようなことも易しく学んでいくことができます。RMIにはパラレルプログラミングを容易に実現するために必要となる非同期メソッド機能がサポートされていません。また、CORBA製品のユーザーの方々も、CORBA非同期メソッドの仕様があまりにも複雑すぎて、使ってみようと思われる方は少なかったでしょう。
HORBを使えば、パラレルプログラミングが非常に身近に感じることができるでしょう。さあ、HORB通じて、パラレルプログラミングの世界にチャレンジしましょう。今回の説明に使用するサンプルコードは、ここからダウンロードできます。
■非同期メソッド -Oneway呼び出し-
最初にパラレルプログラミングの基礎となるHORBの非同期メソッドを紹介します。非同期メソッドとは、あるオブジェクト(HORBの場合リモートオブジェクト)のメソッドを呼び出し、そのメソッドの処理が完了する前に、呼び出し側の方でも何かほかの処理ができるというものです。
非同期メソッドを理解するために、まず、通常のメソッド呼び出し(同期メソッド)を見てみましょう。例えば、非常に時間がかかる仕事をサーバ側で行わせるようなHORBアプリケーションを考えてみましょう。ここで、時間のかかるメソッドはlongJob()という名前にしましょう。アプリケーションからリモートメソッドのlongJobを呼び出すと、アプリケーションはlongJobメソッドの処理が終わるまで待ち続けます。このようにメソッド処理を待つということは、通常のJavaを使ったメソッド呼び出しの常識ですよね。いままで説明してきたHORBのプログラムでもそうでした。Client1.java(リスト1)とRemote1.java(リスト2)をご覧ください。この2つのプログラム自身は非同期メソッドの検証用であるためlongJobメソッドの中の長い処理はThreadのsleepメソッドを使って代用しています。
public class Client1{ |
リスト1 Client1.java [examples\step6\] |
public class Remote1{ |
リスト2 Remote1.java [examples\step6\] |
このプログラムをHORBを使って実行すると実行結果が図1のようになります(実行結果の左横には、表示される順番を示しています)。
クライアント側の実行結果
サーバ側の実行結果
|
||||||||||||||||||||||||||||||
図1 実行結果 |
このようにリモートメソッドが終了しない限りクライアントの処理は実行できないというのが通常のメソッド呼び出し(同期メソッド)です(図2)。
![]() |
図2 クライアントはlongJobメソッドが終わるまで待ち状態となる |
ここで分散オブジェクトの利用法をじっくり考えてみてください。分散オブジェクトを使う場合、リモートオブジェクトのメソッドの処理は、サーバ側のCPUで処理されます。となると、その処理をサーバCPUに任せてしまい、自分自身(クライアントアプリケーション)は、何か異なる処理をやることでシステムのスループットを向上させたいと考えたくなりませんか。
そこで、非同期メソッド呼び出しの出番となります。HORBは、2つの非同期メソッド呼び出しをサポートしています。その中で最も簡単なOnewayを使ってみましょう。使い方は簡単です。リスト2のlongJobメソッドの名前の最後尾に_OneWayを付けてください。このおまじないを付けてHORBコンパイルを通すだけで、通常のメソッドが非同期メソッドに変化します。呼び出し側もlongJob_OneWay()に書き換えてください。
Client2.java(リスト3)、Remote2.java(リスト4)に、この部分を修正したソースコードを示します。
public class Client2{ |
リスト3 Client2.java [examples\step7\] |
public class Remote2{ |
リスト4 Remote2.java [examples\step7\] |
実行結果は、図3のようになります。実際に動かしてみて、実行順序が異なることを確認してください。
クライアント側の実行結果
サーバ側の実行結果
|
||||||||||||||||||||||||
図3 実行結果 |
いかがですか、図3の実行結果を見ると、サーバ側の2つのメソッドとクライアントの処理が同時に実行されているのが分かりますよね。このようにHORBを使えば、とても簡単に非同期メソッド呼び出しが実現できるのです。しかし、非同期メソッドで戻り値が欲しくなったときはどうすればよいのでしょうか。実は、Oneway呼び出しでは戻り値を得るようなことはできません。このような戻り値が必要な非同期メソッド呼び出しには、HORBのAsyncメソッド機能を使います。
![]() |
図4 クライアントとlongJob()の呼び出しが同時に実行されている |
■戻り値をもつ非同期メソッド呼び出し -Asyncメソッド-
次は、戻り値付きの非同期メソッドを説明します。戻り値付きの非同期メソッドは、メソッド最後尾に_Asyncを付けるようにします。もしlongJobメソッドをAsyncメソッドにするならlongJob_Async()というように変更するだけでOKです。Remote3.java(リスト5)をご覧ください。引数としてサーバでの処理時間(実際には待ち時間)を受け取り、戻り値にはそれをそのまま返しています。
public class Remote3{ |
リスト5 Remote3.java [examples\step8\] |
戻り値付き非同期メソッドを使うアプリケーションはリスト6のようになります。少々複雑なのでリスト中に行番号を入れています。
先ほど説明したRemote3.java(リスト5)をHORBコンパイル(horbc)すると、生成される代理オブジェクト(Remote3_Proxy)に、下記のメソッドが自動付加されるようになります。
public ResultAsync longJob_Request(int time) | 非同期メソッドの呼び出し |
public int longJob_Receive(ResultAsync obj) | 非同期メソッドの戻り値取得 |
public int longJob_Receive(ResultAsync obj,long timeout) | タイムアウト付き、 非同期メソッドの戻り値取得 |
非同期メソッド呼び出しと戻り値取得は、これらのメソッドを使って実現します。longJob_Asyncメソッドを直接呼び出しても同期呼び出しにしかなりません。このメソッドは非同期として定義されたメソッドを同期的に呼び出したいというときに有効となるものです。さて、リスト6を説明しましょう。
001:import horb.orb.*; |
リスト6 Client3.java [examples\step8\] |
●12行目…非同期メソッドの呼び出し
remote.longJob_Request()により非同期メソッドを呼び出します。この呼び出しはすぐさまリターンします。このメソッドの戻り値は、horb.orb.ResultAsyncクラスの配列(8行目で定義)の要素となっています。ResultAsyncは、非同期呼び出しの戻り値が将来入ってくるために用意されるオブジェクトなのです。そのような性質であるためFutureオブジェクトと呼ばれます。非同期メソッドを呼び出した後は、戻り値として返却されたResultAsyncオブジェクトを使って、サーバ側で動作した結果の戻り値を取得することができるのです。
●16行目…クライアント側での処理
longjob()呼び出しからすぐさま戻ってくるので、クライアント側での処理を行います。 ここでは、3秒間のダミー処理を入れています。
●21行目…戻り値の処理
ResultAsyncオブジェクトをlongJob_Receive()の引数として渡します。すると、longJob_Receiveメソッドの結果として、longJob_Asyncメソッドで期待する戻り値が取得できます。もし、ResultAsyncオブジェクトに対応する非同期メソッドの処理がサーバ側でまだ完了していないときは、longJob_Receiveメソッドは、その完了を待たされます。なお、この処理は、ResultAsyncオブジェクトだけを利用する下記のコードでも同等の結果を得ることができます。
int time = ra[i].receiveInt();
上記のコードは、longJob_Receiveメソッドを使わない分コーディングに自由度をもたらします。しかし、下記のようなデメリットもあります。
- サーバ側の例外をJava例外処理として取得できない
- 戻り値の型保証ができない
(基本データ型(例えばintとInteger)はすべて基本データ型(intなど)で返却される/上記以外のクラスオブジェクトはすべてjava.lang.Objectとして返却される)
実行結果は、図5のようになります。まずサーバ側の非同期メソッドとクライアントの処理が並行的に開始され((1)〜(11))、その後、処理時間の短いメソッドから順次完了((12)〜(22))します。そしてクライアントに結果が戻ります ((23)〜(32)) 。
この結果のように非同期呼び出しではサーバ側のメソッド呼び出し順は必ずしもクライアントの非同期メソッド呼び出し順どおりになるわけではありません。例えば(5)はクライアントでは一番先に呼び出されていますが、サーバ側の処理は若干遅れて開始されています。
クライアント側の実行結果
サーバ側の実行結果
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
図5 実行結果 |
いかがでしょうか。非同期メソッドの戻り値を取得するのも簡単でしょう。ただ、図5の実行結果をよく観察すると、処理結果に問題があることにお気付きになられるでしょう。その問題は、下記のような要求が出されたときに明確になります
「サーバ側の処理が完了したものから順に戻り値を受け、クライアントで処理したい!」
上記の要求には、このサンプルのやり方では対応できません。 なぜなら、このサンプルでは一番長い処理(1000ms)を最初の非同期メソッドで呼び出しています(リスト6の12行目)。そして、そのResultAsyncを引数にして、longJob_Receiveメソッドで戻り値を受け取っています。longJob_Receiveメソッドは、サーバ側の処理完了を待ちますので、結果として一番長い処理(1000ms)の完了を待ってしまうという問題があるのです。完了したResultAsyncからクライアント側で使いたいという高度な要求には耐えられないわけです。そこでサーバ側の処理が完了したかを確認(ポーリング)するようなコードに書き直してみましょう。Cleint4.java(リスト7)の24行目にあるように、サーバ側の非同期メソッドの対象となるResultAsyncオブジェクトにisAvailableメソッドを送り、trueが返却された場合、サーバ側の非同期メソッドは完了しているので、完了しているものだけをクライアント側で処理すればいいわけです。これを動かすと、図6の実行結果となります。
001:import horb.orb.*; |
リスト7 Client4.java [examples\step8\] |
クライアント側の実行結果
サーバ側の実行結果
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
図6 実行結果 |
この実行結果のとおり、ほぼサーバ処理が完了した順にクライアント側で戻り値に対して処理ができていると思います。しかし、やはりポーリングする前にクライアントの処理を行う((2)と(14))ため、クライアントの結果取得の順序が逆になってしまっています((15)と(16))。
また、ポーリング処理(リスト7の21行目から)が面倒で分かりづらいコードとなっています。さらに、クライアントからいちいちサーバ側の処理が終わっているかどうかをポーリングしているようでは、クライアント側で別の処理を行うことが難しいという問題があります。この問題を解決するため、HORBには、Asyncメソッドのコールバック機能というものがあります。
■Asyncメソッドのコールバック呼び出し
コールバックとは、非同期メソッドが完了したことをリモートオブジェクトからクライアントに通知させることです。これによって、クライアントはlongJobメソッドの処理とその結果取得は別のスレッドに任せることができ、自分自身の処理に集中できるようになります。Client5.java(リスト8)にソースコードを示します。
001:import horb.orb.*; |
リスト8 Client5.java [examples\step8\] |
リスト8の説明は以下のとおりです。
●2行目・21行目…コールバックされるオブジェクトの設定
HORBの非同期コールバックを使うには、非同期メソッドの処理が終わったときに誰に通知するか設定する必要があります。21行目のremote._setHandler(this,
i+1)部分で通知されるオブジェクトとしてthisを設定しています。コールバックはrunメソッドにより通知されます(6行目)。runメソッドは、2行目のhorb.orb.AsyncMethodHandlerインタフェースによって定義されているメソッドをオーバライドします。
●6行目…runメソッド
runメソッドの引数は、ResultAsyncとint型のタグです。最初の引数は、非同期メソッドの結果を保有しているFutureオブジェクトが飛んできます(23行目で返されるResultAsyncと同じもの)。次の引数は、どの呼び出しかを指示するタグ(21行目で指定したタグ)となります。
●8行目、9行目…非同期メソッド呼び出しの結果取得(Futureオブジェクトの使い方)
非同期メソッド呼び出しの結果は、ResultAsyncオブジェクトに格納されています。これをリスト7の26行目のように代理オブジェクト(Remote_Proxy)のlongJob_Receiveメソッドで取得してもいいのですが、ここではResultAsyncオブジェクトだけで結果を取得しています。
ra.receiveInt();
ResultAsyncには、以下のような戻り値取得メソッドが用意されています。先に述べたように基本データ型をラップしている標準クラスは、基本データ型として取得されます。たとえば、Byteが戻り値の場合、receiveByteメソッドを使ってbyte型で取得しなければなりません。
Object receiveObject()
String receiveString()
boolean receiveBoolean()
byte receiveByte()
char receiveChar()
double receiveDouble()
int receiveInt()
float receiveFloat()
long receiveLong()
また、ResultAsyncだけではサーバ側で発生した例外を自動検出できませんので、下記のメソッドを利用して例外オブジェクトを取得します(サーバ側で発生した例外は、代理オブジェクトのlongJob_Receiveメソッドを使えば通常のJava例外処理と同様の方法で取得することができます)。
boolean isException()
Throwable receiveException()
さて、実行結果は、図7のとおりです。早く終わった非同期メソッドの順にクライアント側でコールバック処理がなされているのがお分かりになるでしょう。先に説明しましたが、このプログラムは長い処理ほど先にメソッド呼び出しされるようにしています。実行結果では、後方で呼び出され、最も早く処理を終える非同期メソッドの戻りがResultAsyncとして、クライアントのコールバック処理に渡されています。
さて、ここでrunメソッドはどのように呼び出されているか考えてみてください。図7のクライアント処理の開始(2)から終了(16)の間にコールバック処理((13)と(15))が行われています。この部分を見ると、クライアントの処理と並行してコールバック処理が呼び出されていることがお分かりになるでしょう。そうです、コールバック処理はHORBによって生成されたクライアント処理とは別のスレッドを使って呼び出されているのです。よって、コールバック処理はクライアントの処理の中でデータを共有している際には、そのデータを排他制御しなければならないケースが発生します(複数スレッドにおける同時実行性の保証)。今回の例では、このような排他制御は必要ありませんでしたが、リスト8の6行目のrunメソッドにsynchronizedを付与している意味は、コールバックメソッドも同時に呼び出しされる可能性があるため、競合を避けるためsynchronizedメソッドにすることでスレッドセーフにしています。ただ、今回のケースでは、runの引数だけを使っていますのでスレッドセーフです。また、System.out.printlnもメソッド内部でsynchronizedされるので表示が乱れたりすることはありません。よって、特にrunメソッドをsynchronizedメソッドにする必要もありません。
クライアント側の実行結果
サーバ側の実行結果
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
図7 実行結果 |
(2)いよいよ並列プログラミング![]() |
Index | |
![]() |
(1)非同期メソッドを理解する 非同期メソッド -OneWay呼び出し- 戻り値をもつ非同期メソッド呼び出し -Asyncメソッド- Asyncメソッドのコールバック呼び出し |
(2)いよいよ並列プログラミング 複数台のサーバのオブジェクトを実行 実行結果 |
|
![]() |
連載記事一覧 |
- 実運用の障害対応時間比較に見る、ログ管理基盤の効果 (2017/5/9)
ログ基盤の構築方法や利用方法、実際の案件で使ったときの事例などを紹介する連載。今回は、実案件を事例とし、ログ管理基盤の有用性を、障害対応時間比較も交えて紹介 - Chatwork、LINE、Netflixが進めるリアクティブシステムとは何か (2017/4/27)
「リアクティブ」に関連する幾つかの用語について解説し、リアクティブシステムを実現するためのライブラリを紹介します - Fluentd+Elasticsearch+Kibanaで作るログ基盤の概要と構築方法 (2017/4/6)
ログ基盤を実現するFluentd+Elasticsearch+Kibanaについて、構築方法や利用方法、実際の案件で使ったときの事例などを紹介する連載。初回は、ログ基盤の構築、利用方法について - プログラミングとビルド、Androidアプリ開発、Javaの基礎知識 (2017/4/3)
初心者が、Java言語を使ったAndroidのスマホアプリ開発を通じてプログラミングとは何かを学ぶ連載。初回は、プログラミングとビルド、Androidアプリ開発、Javaに関する基礎知識を解説する。
![]() |
|
|
|
![]() |