はじめに
Javaで、ささっと多重処理ができないものかと調べていたときに、Streamで「Parallel」なるものがあったので試し書きしたときのコード込みでまとめとこうと思った記事です。
個人で黙々と小さめなプログラムを書いているときは、取り扱うデータ量も大したことがないことが多いんですけどね。
開発環境
以下の環境で試しています。
- macOS Big Sur
- Intel Core i7 (6Core) (Hyper Threading有)
- Java 11
macOSで動かしていますが、コード自体はJavaがインストールされている環境であれば動くと思います。後述しますが、CPUのコア数が並列処理を行う上で影響があります。
実装
実装の概要
BaseStream.parallel()を使った並列処理を実装してみます。メソッド「printNumber」は処理時間がかかるようにThread.sleepを使って3000msスリープするように実装しています。
ソースコード
class Parallel {
public static void main(String[] args) {
List<Integer> numbers = List.of(1, 2, 3, 4, 5);
// 直列処理をした時の時間計測。
// printNumberをコールするたび3秒かかるため、要素数(5) * 3sec かかる計算。
long start = System.currentTimeMillis();
numbers.stream().forEach(Parallel::printNumber);
long end = System.currentTimeMillis();
System.out.println("直列処理時間: " + (end - start) + "(ms)"); // 15000ms前後を想定
// 並列処理をした時の時間計測
// 要素数(5)を並列処理するので、3secで実行完了する想定
start = System.currentTimeMillis();
numbers.parallelStream().forEach(Parallel::printNumber);
end = System.currentTimeMillis();
System.out.println("並列処理時間: " + (end - start) + "(ms)");
}
private static void printNumber(Integer number) {
try {
// 無意味なスリープ
Thread.sleep(3000);
} catch(Exception e) {
e.printStackTrace();
}
System.out.println(String.format("number: %d", number));
}
}
このソースコードのキモの部分を簡単に。
- 8行目: 通常のStreamを使って繰り返し処理(1つ目の処理が完了したら次の処理を実行)
- 16行目:stream().parallel()を使って繰り返し処理(スレッドを作れるだけ作って並行処理)
やっている違いはこれだけです。このように記述するだけで並列処理が実装できます。
実行結果
parallel % javac Parallel.java
parallel % java Parallel
number: 1
number: 2
number: 3
number: 4
number: 5
直列処理時間: 15055(ms)
number: 3
number: 5
number: 4
number: 2
number: 1
並列処理時間: 3013(ms)
ほぼソースコードコメントに書いた通りの想定処理時間となりました。
注意点
結果をご覧いただければわかるように、並列処理は処理順が保証されないです。
並列数について
先ほど、ソースコード解説のところで「スレッドを作れるだけ作って並行処理」と書いたのですが、スレッド数はどれだけ作られるのでしょう?
軽く調べていたら「Baeldung」で取り上げられてました。並列ストリームは特に何も設定しなければ、共通スレッドプールを使うらしく、その数は「CPUコア数 – 1」となるようです。
試してみよう
ほんとかどうかを確認するには実際にコードを書いて動かした方が早いですね。
前述の通り、私の環境では「CPUのコア数 = 6」かつ「Hyper-Threading」が効くのでメインスレッド以外に別途11スレッド(6 * 2 – 1)起動する想定です。実処理ではメインスレッドも動作するので12スレッドが並行動作するはず。
下のコードは、想定する最大スレッド数(12)と同じ要素数のコレクションと、想定する最大スレッド数を超える(13)要素数のコレクションを処理させてみたプログラムです。
class Parallel2 {
public static void main(String[] args) {
// コア数6 * 2(HT) => 12スレッドの想定、3秒で終わる計算
System.out.println("要素数 == スレッド数");
executeParallel(12);
// スレッド数より1多い要素数
// 1回の並列処理では処理できないので、13要素目のみ次処理になる(=6秒)想定
System.out.println("要素数 > スレッド数");
executeParallel(13);
}
private static void executeParallel(int elementCount) {
List<Integer> numbers = IntStream.rangeClosed(1, elementCount)
.mapToObj(Integer::valueOf).collect(Collectors.toList());
// 並列処理を実行
long start = System.currentTimeMillis();
numbers.stream().parallel().forEach(Parallel::printNumber);
long end = System.currentTimeMillis();
System.out.println("並列処理時間: " + (end - start) + "(ms)");
}
private static void printNumber(Integer number) {
try {
Thread.sleep(3000);
} catch(Exception e) {
e.printStackTrace();
}
System.out.println(String.format("number: %d", number));
}
}
実行結果
要素数 == スレッド数
number: 9
number: 8
number: 5
number: 3
number: 10
number: 2
number: 12
number: 11
number: 7
number: 4
number: 1
number: 6
並列処理時間: 3011(ms)
要素数 > スレッド数
number: 9
number: 5
number: 4
number: 13
number: 7
number: 11
number: 6
number: 3
number: 8
number: 2
number: 1
number: 12
number: 10
並列処理時間: 6008(ms)
おぉ。想定通り。
でも、ハードウェア環境が違うと結果が変わってくるよね。コア数が少ない環境で動作させた場合、性能改善に対して影響が出ない可能性もありますね。
最後に
はじめに、でも書きましたが個人で小さくプログラムを書いているときは、あまり並列処理とかを考えなくともなんとかなるケースが多いので、ひょんなことから学ぶ機会・試す機会が生まれたことに感謝してます。
Java Goldでも並列処理は試験範囲になっているのだけど、実際に自分で書いて動かしたりしてみないと、正直頭への入り方が全然違うので試験勉強にもなった気がしています。(試験を受ける予定は今の所何もないけれど)
今回はCPUの性能フルフルに使った内容でしたが、スレッド数を指定して実行できるようなので、そちらもサンプルコード付きの記事を書こうかな。
コメント