67794


現在Javaで非同期処理を勉強しています。CompletableFutureを見ています。

以下のようなコードを書いたのですが、非同期処理より同期処理の方が速くなります。  
簡単な処理だからでしょうか？初期化や設定に時間がかかるため今回は同期処理の方が速くなったのでしょうか？  
またどういった処理あたりから非同期処理の方が速くなるのでしょうか？  
明確な違いではなく、ご経験からの感想やご意見で構いません。よろしくお願いいたします。

[code]

    public class CompletableFutureSample {
        public static void main(String[] args) {
    
            long start = System.currentTimeMillis();
    
            CompletableFuture<Integer> cf = CompletableFuture.supplyAsync(() -> 1 * 100 * 35 * 42 * 75);
            CompletableFuture<Integer> cf2 = CompletableFuture.supplyAsync(() -> 2 * 100 * 35 * 42 * 75);
    
            cf.thenAcceptBoth(cf2, (c1, c2) -> {
                System.out.println("cf :" + c1 + ", cf2 : " + c2);
                System.out.println("result : " + (c1 + c2));
            });
    
            //      Integer i1 = 1 * 100 * 35 * 42 * 75;
            //      Integer i2 = 2 * 100 * 35 * 42 * 75;
            //
            //      System.out.println("cf :" + i1 + ", cf2 : " + i2);
            //      System.out.println("result : " + (i1 + i2));
    
            long end = System.currentTimeMillis();
            System.out.println((end - start) + "ms");
    
        }
    }
    
[/code]

非同期（コメントなし箇所）  
cf :11025000, cf2 : 22050000  
result : 33075000  
134ms

同期（コメントアウト箇所）  
cf :11025000, cf2 : 22050000  
result : 33075000  
1ms


CC BY-SA 4.0
2020-06-18T12:31:45.423
67787
2020-06-21T15:45:43.577
40231
question
java java8 非同期
非同期処理が同期処理より遅いことについて理解したい


非同期は計算資源が豊富にあって、プロセスに長い処理と短くて回数のある処理を流したい場合に有効ですよね。これは、長い処理がリソースを占有し、短い処理がそれを待つということをするからです。この場合、両者に依存がなければ、非同期とすることでリソースを有効活用できますが、管理コストが乗ってきます。仮に短くてリソースを占有しない処理を非同期としても短くなるどころか管理コストだけ増えてかえって遅くなることも想定されます。ご呈示の処理は管理コストに見合うものでしょうか。とても軽くてあまり効果が出ないような処理に見えます。それぞれの処理でどういったスレッド構成になっているかなど、frightrecorderやvisual
vmを利用して観察すると面白いかと思います。

また、一回だけの計測ではその他の要因（たとえば、同一PCで走っている別のプロセスへのリソース割り当て）に影響されるので、複数回とって平均を観察するなどされると精度の良いデータが得られるのではないでしょうか。（すでにやっていらっしゃるのかもしれませんが。）

それと、非同期自体を「早くするため」に使うよりも「とりあえず応答しておく」ために使ったりすることも知っておくと面白いかもしれません。


CC BY-SA 4.0
2020-06-18T17:33:53.027
67792
2020-06-18T17:33:53.027
10174
67787
answer


> 簡単な処理だからでしょうか？初期化や設定に時間がかかるため今回は同期処理の方が速くなったのでしょうか？

そうだと思います。  
今回のケースでは、準備にかかる時間と比較して、計算に使った時間は無視できるほど短いと思います。  
非同期処理に134msかかっていますが、ほとんど準備に使われていると思います。

> またどういった処理あたりから非同期処理の方が速くなるのでしょうか？

入出力待ちなど、CPUの実行権を放棄する処理を並行する場合に効果が出ると考えます。ただし、CPUの実行権を放棄する時間が準備に要する時間より短い場合、効果は期待できません。

なお、大量にCPUを使う計算処理の場合は、CPUの数によっては非同期処理の効果が出ると思います。


CC BY-SA 4.0
2020-06-18T23:47:42.323
67794
2020-06-18T23:47:42.323
35558
67787
answer


今回のコードの場合、実行時間の差の一番の原因は同期/非同期がどうこう、というよりも、やっていることが違うからです。

[こちら](https://ja.stackoverflow.com/a/67221/2808)でも触れていますが、[`CompletableFuture#supplyAsync()`](https://docs.oracle.com/javase/jp/11/docs/api/java.base/java/util/concurrent/CompletableFuture.html#supplyAsync\(java.util.function.Supplier\))は`ForkJoinPool.commonPool()`を利用します。  
質問文のコードにこれが現れるように書き換えると次のようになります:

[code]

    public static void main(final String[] args) {
    
        // 並列レベルは ForkJoinPool.getCommonPoolParallelism()
        final Executor es = ForkJoinPool.commonPool();
        // 並列レベル1, つまり直列実行
        // final Executor es = new ForkJoinPool(1);
    
        final long start = System.currentTimeMillis();
    
        final CompletableFuture<Integer> cf = CompletableFuture.supplyAsync(() -> 1 * 100 * 35 * 42 * 75, es);
        final CompletableFuture<Integer> cf2 = CompletableFuture.supplyAsync(() -> 2 * 100 * 35 * 42 * 75, es);
    
        cf.thenAcceptBoth(cf2, (c1, c2) -> {
            System.out.println("cf :" + c1 + ", cf2 : " + c2);
            System.out.println("result : " + (c1 + c2));
        });
    
        final long end = System.currentTimeMillis();
        System.out.println((end - start) + "ms");
    }
    
[/code]

このコードと同等の処理を同期的に実行するには、並列レベルを`1`として生成した`ForkJoinPool`を使うことで実現できます。  
(上記コード中のコメントアウト部分に差し替えることで実現できます)

これらを比較すると差異はほとんど無くなるのではないかと思います。

簡単な処理をマルチスレッド化するとむしろ遅くなる、というのはその通りですが、今回のように1回だけ2つの処理を非同期で実行した程度では影響は誤差レベルです。

* * *

なので回答としては、

  * 非同期実行フレームワークの構築にかかる時間が無視できない程度の小規模なプログラムであるため。(コメントアウト部分ではそれを行っていない分速い)

ということになります。

* * *

ちなみに、このコードも

  * [非同期処理において結果が思ったように出ない理由を知りたい](https://ja.stackoverflow.com/q/67200/2808)

と同様、`cf`,`cf2`が完了するまでプロセスが生きている保証はないです。

[code]

    System.out.println((end - start) + "ms");
    
[/code]

は質問者の想定している処理時間を表していません。  
(そのように見えるのはたまたまです)


CC BY-SA 4.0
2020-06-19T04:31:30.190
67803
2020-06-21T15:45:43.577
2020-06-21T15:45:43.577
2808
2808
67787
answer


基本的には非同期の方が並行処理の為のコストがかかるので全体としてはパフォーマンス下がりますよ。  
実際はスレッドプールやらなんやら色々あるのですが、誤解を恐れずイメージを伝えますと、  
並行処理を行うコストが１０秒かかるとして、

・１秒かかる処理を４つ  
同期処理４秒（１x４）  
非同期処理１１秒（１＋１０）

・２０秒かかる処理を４つ  
同期処理８０秒（２０x４）  
非同期処理３０秒（２０＋１０）


CC BY-SA 4.0
2020-06-20T22:46:06.650
67846
2020-06-20T22:46:06.650
40751
67787
answer


非同期処理は計算処理を速くするのではなく、計算リソースを効率よく使うための手段です。IO等のCPU処理速度とは異なるところに律速が存在し、次の計算に待ち(wait)が発生するような状況でこそ効果を発揮します。

大きさの異なる三つのファイルA(100MB)、B(10MB)、C(50MB)をそれぞれ読み取るとします。これらはそれぞれ別のファイルシステム上にあるため単独でも同時でも読み取り速度は10MB/sとします。読み取ったファイルの大きさによって10MBあたり1sかかる処理を実行し、ファイル毎の値を求めます。最後に、三つのファイルの値の合計する(1s)、そんなプログラムを仮定します。

同期処理は次のようになります。(+Xs / YsはXsがその処理にかかった時間でYsがこれまでの時間)

  1. Aを読み取る。+10s / 10s
  2. Aを処理する。+10s / 20s
  3. Bを読み取る。+1s / 21s
  4. Bを処理する。+1s / 22s
  5. Cを読み取る。+5s / 27s
  6. Cを処理する。+5s / 32s
  7. 合計する。 +1s / 33s

このとき1,3,5はIO処理がメインでCPUはほとんど使っていません。かなり無駄が多いことになります。では、非同期の場合はどうでしょう。非同期処理を始めること自体はゼロコストではないため、ファイルの読み取り開始自体は1sかかると想定します。読み取りはバックグラウンドで行われ、メイン側のCPU資源は使わない(または、無視できるほど小さい)とします。

  1. Aの読み取り開始。+1s / 1s (11sに完了)
  2. Bの読み取り開始。+1s / 2s (3sに完了)
  3. Cの読み取り開始。+1s / 3s (8sに完了)
  4. 完了したBを処理する。+1s / 4s
  5. waiting... +4s / 8s
  6. 完了したCを処理する。+5s / 13s
  7. 完了したAを処理する。+10s / 23s
  8. 合計する。 +1s / 24s

途中の5.で待ちがありましたが、9sの短縮になりました。これは単純なモデルですが、同時に処理したいIOが多数あり、それぞれの処理時間がバラバラというときは上記よりももっと効率的に動作する場合があります。

おまけでマルチスレッドによる並列にした同期処理を見てみましょう。CPU資源は1つのみだが、IO処理はバックグラウンドで並列可能とします。スレッド自体の開始も1sとします。まずは、コンテキストスイッチは1s毎に動作し、0コストであると仮定します。

  1. [メイン]A用のスレッド開始。+1s / 1s
  2. [メイン]B用のスレッド開始。+1s / 2s  
[A]Aの読み取り(1/10)

  3. [メイン]C用のスレッド開始。+1s / 3s  
[A]Aの読み取り(2/10)  
[B]Bの読み取り(1/1)

  4. [A]Aの読み取り(3/10) +1s / 4s  
[B]Bを処理(1/1)  
[C]Cの読み取り(1/5)

  5. [A]Aの読み取り(4-7/10) +4s / 8s  
[C]Cの読み取り(2-5/5)

  6. [A]Aの読み取り(8-10/10) +3s / 11s  
[C]Cの処理(1-3/5)

  7. [A]Aの処理(1/10) +1s / 12s
  8. [C]Cの処理(4/5) +1s / 13s
  9. [A]Aの処理(2/10) +1s / 14s
  10. [C]Cの処理(5/5) +1s / 15s
  11. [A]Aの処理(3-10/10) +8s / 23s
  12. [メイン]合計する。 +1s / 24s

トータル時間は非同期と変わらないように見えますが、実際はこれにコンテキストスイッチによる負荷が発生します。。1sあたり0.1sほど負荷があるとしても、24s*1.1=26.4sです。このコンテキストスイッチによる負荷の大きさが非同期との大きな違いです。

上は単純でかつ恣意的なモデルですので、計算モデルの取り方によっては全く異なる可能性もあります。複数CPUが搭載されている場合、Javaではマルチスレッドで並列化した方が計算資源を効率よく使えるかも知れませんが、PythonやRubyではGIL(GVL)の効果でほぼ無意味かも知れません。ただ、同期処理と非同期処理、シングルスレッドとマルチスレッド、それらを比較したい場合は、純粋な計算処理ではなく、ファイルの読み取りやWebアクセスと言ったIO処理を混ぜた場合にどうなるかを見てみると、どういうときに効果的かというのが見えてくると思います。


CC BY-SA 4.0
2020-06-21T10:37:47.410
67865
2020-06-21T10:37:47.410
7347
67787
answer