2019年2月20日水曜日

JavaのCompletableFutureによる並行処理

 Javaでは当初から、平行プログラミングが重視されてきました。近年のコンピュータはマルチコア化(メニコア化)が進んでいるので、マルチスレッドによる並列化はさらに重要性を増しています。最近のJavaは、強固なコンカレントパッケージを提供していて、高レベルで、スレッドセーフなコレクションや、スレッドプールを利用できます。これによって、ユーザは、明示的にスレッドを開始することもなく、またロックを使用することもなく、並列プログラムを書くことができるようになってきました。

 ここでは、これらのうちで、FutureクラスとCompletableFutureクラスを取り上げます。下記のJava書籍[1]の第12章には、「マルチスレッドと平行処理」が詳しく解説されています。その中にある例題を参考にして検討します。例えば、以下の図1ような図を描くとします。まず、5角形と8角形をそれぞれ別のスレッドで並行してして描きます。それら両方の描画が完了するのを待って、次に外側に4角形を描きます。この四角形の一辺の長さは、図に示したように、5角形と8角形を描き終わった後に決まるとしています。(短いビデオもご覧ください。)


図1 5角形と8角形を並列に描いた後に、外側に四角形を描く

これらの多角形を描くメソッドpolyDとpolyVを以下のように定義します。
 double polyD(int x, int y, int n, int s)
 void polyV(int x, int y, int n, int s)
  x, yは多角形描画の開始点の座標
  nは頂点の数
  sは一辺の長さ
  polyDは、n * sの値を返します。

 まず、ExecutorとFutureを使ったプログラム(リスト1)を以下に示します。ExecutorService(スレッドプール)を使って、2つの多角形の描画を別々のスレッドへsubmitします。(line 21-23)すると、Futureオプジェクトx1, x2が直ちに返ってきます。したがって、描画を待たずに、両方の多角形の周の長さの和を求める計算(line 25)の開始に向かいます。しかし、ここで、x1.get()とx2.get()が呼び出されていることに注意しましょう。すなわち、このget()は、それぞれの多角形の描画の完了を待つことを意味します。それが終わった後に、外側の四角形が描かれます。(line 26)

リスト1 ExecutorとFutureによる、並列描画+逐次描画

 次に、文献[2]などを参考にして、CompletableFutureを使ったプログラム(リスト2)を以下に示します。これは、上記のExecutorとFutureを使ったプログラムと完全に同じ動作をします。しかし、注目すべきは、これが1ステートメントで完結していることです。Threadプールの利用も、2つの多角形描画の待ちも明示されていません。5角形と8角形の描画のための2つのCompletableFutureが、.thenCombineAsynによって連結され、さらにその後に起こる、四角形の描画も.thenAcceptで連結されています。並行処理部分があるのに、すべてが一気にパイプライン連結されています。なお、line 34は、両方の多角形描画の後に得られる、周の長さの和を計算するラムダ式です。

リスト2 CompletableFutureによる、並列描画+逐次描画
(全体が1ステートメントで完結)

 リスト1も十分明快ではありますが、このリスト2の方が、さらに洗練されたエレガントなプログラムであると言えるのではないでしょうか!別の言い方をすると、リスト1は、「ここで待て。そこで計算して、その結果を使って次の描画をせよ。」という命令的なのに比べて、リスト2は「2つの並行処理が終ったらその結果を使って次の描画を行う。」という仕様的記述だとも言えます。もちろん、状況により、使い分けることになるでしょう。

 リスト1とリスト2の両方に言えることですが、Java8で導入されたLambda Expressionsが重要な役割を果たしていることも再認識できると思います。

 さらに複雑な図2のような並列描画プログラムも、このCompletableFutureを使って書いてみます。これは、図1の描画を2つ横に並べて同時に描かせます。すなわち、さらに多重の並列プログラムになっています。

図2 上記の図1の並列描画をさらに2つ同時に描く

具体的なやり方は色々あるでしょうが、ここではリスト3のように、図1相当のものを2つ、.thenCombineAsyncで連結しました。なお、図1の描画に相当するプログラムは、メソッドCPF(ss)として設定しました。ここで、引数ssは、各図の描画を開始位置を決めるためのシフト量です。(なお、line 43のラムダ式(x,y)->y-xは、念のため、外側の図の開始位置の差を確認し、その値をline 44で表示していますが、特別な意味はありません。)

リスト3 図2を描くためのCompletableFutureを使ったプログラム
リスト3 図2を描くためのCompletableFutureを使ったプログラム(続)

■参考書籍
[1]立木秀樹、有賀妙子著「すべての人のためのJavaプログラミング 第3版」、共立出版
[2]C.S. Horstmann(柴田芳樹訳)「Java SE 8 実戦プログラミング」、インプレス

0 件のコメント:

コメントを投稿