Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save asufana/9687094 to your computer and use it in GitHub Desktop.
Save asufana/9687094 to your computer and use it in GitHub Desktop.
PlayFramework1での非同期・並列処理

PlayFramework1での非同期・並列処理

PlayFramework1でのFuture/Promiseの説明をしようと思ったら大事になったでござる。

HTTPのC10K問題

Life is beautiful : node.js と thread hog の話

  • CPUが性能向上して多くのリクエストを処理できるようになってくると、HTTPサーバのC10K問題が健在化してくる(@IT : Web2.0の先にあるC10K問題

  • HTTPリクエストに対して1つずつプロセスを生成してたらプロセス数上限(32767)に引っかかるよね

  • プロセスは増やさないでスレッドで処理するには、マルチスレッドで並列処理が可能なプログラムを書く能力が必要だよね

  • でもスレッドがプロセスより軽いと言っても1万スレッド作ったら20GBかかるよ

  • あとマルチスレッドはスレッド切り替え(コンテキストスイッチ)コストが大きいよ

  • シングルスレッドで非同期処理(並列処理)がいいんじゃね ← いまここ

イベント駆動型アーキテクチャ

IIJ : 高速WebサーバMighttpdのアーキテクチャ

  • apache:プロセス駆動型

  • アクセスに応じてプロセスやスレッドを生成してして対応する

  • プロセスを切り替えるためにコンテキストスイッチが多発する

  • nginx, node.js:イベント駆動型

  • 1コアに付き1つのプロセスでイベントを非同期処理(並列処理)する

  • プロセスを切り替える必要がないためコンテキストスイッチが低減される

ノンブロッキングIO

イベント駆動型はひとつのプロセス/スレッドで多くのリクエストをさばくため、イベントを並列で処理しなければならない。そのためにはそれぞれの処理をノンブロッキングIOによる非同期化させる必要がある。

ノンブロッキングIO

  • データの入出力において、データの送受信の完了を待たずに他の処理を開始できる仕組み
  • ブロッキングIOの場合は処理中は他の処理がブロックされる
  • JDBCアクセスや(NIOを利用しない)ファイル操作などがブロッキングIOの例

補足:非同期IOとノンブロッキングIOは厳密には違うらしい

PlayFrameworkの場合

ノンブロッキングへの熱い情熱

Playスレッドプール設定

Play はとても短いリクエストで動作することを意図しています。Play は HTTP コネクタによってキューイングされたリクエストを処理するために固定のスレッドプールを使用します。最適な結果を得るために、このスレッドプールは可能な限り小さくあるべきです。デフォルトのプールサイズを設定するための最適値として、典型的には プロセッサ数 + 1 を使用します。(PlayFramework : HTTP リクエストの中断

  • DEVモード:1
  • PRODモード:CPUコア数 + 1
  • play status から確認可能

参考

Play1での実装時に意識すること

スレッドをブロックしないようにする

これは、もしリクエストの処理時間がとても長い場合 (例えば、長い計算を待つなど) に、リクエストがスレッドプールをブロックし、アプリケーションの応答性に不利益をもたらすことを意味します。もちろん、プールにより多くのスレッドを追加することもできますが、リソースを浪費する結果となるかも知れませんし、いずれにしてもプールのサイズは決して無限にはなりません。(PlayFramework1 : HTTP リクエストの中断

非同期・並列化処理による処理パフォーマンスの向上

前述した三つの非同期リモートリクエストの例を実装する別の方法は、コールバックを使うことです。ここでは await(…) の呼び出しに、すべての promises が完了した時に実行されるコールバックである play.libs.F.Action の実装が含まれています。(PlayFramework1 : コールバック

Future

Futureとはまだ存在しない計算結果に対するプレースホルダのようなもの

future, promise, delay とは、プログラミング言語における並列処理のデザインパターン。何らかの処理を別のスレッドで処理させる際、その処理結果の取得を必要になるところまで後回しにする手法。処理をパイプライン化させる。(Wikipedia : Future

JavaのFutureインターフェース

java.util.concurrent.Future

FutureはV型の値をラップして、getするとその値が取れる。ただ、Futureは「まだ計算おわてないよー」的な状態も表すので、getしたときにまだ内部の値がない時は値が計算されるまでblockする。cancelとかもできる。(愛と勇気と缶ビール : 個人的Java並行/非同期処理めも

そのまま使うとブロッキングされる(同一スレッドで動作する)ので全然非同期ではない、能動的に別スレッド化する必要がある(大発見 : Futureの使い方とFutureTaskのワナ

Promise

(PlayFrameworkでの)Promiseとは

Promise は Play による Future 型のカスタマイズです。実際のところ、 Promise は Future でもあるので、標準的な Future として使用することもできます。しかし、 Promise にはとても興味深い属性: onRedeem(…) を使って、期待する値が利用可能になるとできるだけ早く呼び出されるコールバックを登録する機能が備わっています。(PlayFramework1 : Javaによる関数プログラミング

PlayではPromiseクラスにより簡単に非同期・並列処理+コールバックが実装できるようになっている

サンプルクラス:Work

Play.Jobを使ってPromiseインターフェースを返却することで簡単に非同期・並列処理化できる

/** とある処理クラス */
public class Work {
    
    /** とある処理(同期) */
    public Work doWork() throws InterruptedException {
        System.out.println(" 実行スレッド名:" + Thread.currentThread().getName());
        System.out.println(" Working...");
        Thread.sleep(3000);
        System.out.println(" Done!");
        return this;
    }

    /** とある処理(非同期・Promiseを返却する) */
    public static Promise<Work> doJobAsync() {
        return new Job<Work>() {
            @Override
            public Work doJobWithResult() throws Exception {
                return new Work().doWork();
            }
        }.now();
    }
}

同期処理テスト

    private long start, stop;
    
    @Before
    public void doBefore() {
        start = System.currentTimeMillis();
        System.out.println("Start");
    }

    @After
    public void doAfter() {
        System.out.println("End");
        stop = System.currentTimeMillis();
        System.out.println("実行にかかった時間:" + (stop - start) + " ミリ秒");
    }

    @Test
    public void testDoJob() throws Exception {
        //同期処理
        new Work().doWork();
        System.out.println("Called");
    }

    // 処理結果
    //Start
    // 実行スレッド名:main
    // Working...
    // Done!
    //Called
    //End
    //実行にかかった時間:3001 ミリ秒

非同期処理テスト

Promise.get()だと別スレッド処理だが、取得できるまで処理ブロックされる

    @Test
    public void testDoJobAsync01() throws Exception {
        //get()で返答を待つ
        final Promise<Work> workPromise = Work.doJobAsync();
        workPromise.get();
        System.out.println("Called");
    }

    // 処理結果
    //Start
    // 実行スレッド名:jobs-thread-1
    // Working...
    // Done!
    //Called
    //End
    //実行にかかった時間:3028 ミリ秒

非同期処理テスト

処理結果の取得が不要な場合

    @Test
    public void testDoJobAsync02() throws Exception {
        //処理は非同期で開始されている
        final Promise<Work> workPromise = Work.doJobAsync();
        System.out.println("Called");
        
        //非同期処理確認のためのwait
        while (true) {
            if (workPromise.isDone()) {
                break;
            }
            Thread.sleep(100);
        }
    }

    // 処理結果
    //Start
    //Called
    // 実行スレッド名:jobs-thread-1
    // Working...
    // Done!
    //End
    //実行にかかった時間:3130 ミリ秒

非同期処理テスト

処理完了時にアクションするならコールバックを設定

コントローラならawait関数を利用

    @Test
    public void testDoJobAsync03() throws Exception {
        //処理は非同期で開始されている
        final Promise<Work> workPromise = Work.doJobAsync();
        System.out.println("Called");
        
        //完了時にコールバック
        workPromise.onRedeem(new F.Action<F.Promise<Work>>() {
            @Override
            public void invoke(final Promise<Work> completed) {
                System.out.println(" Finished");
            }
        });
        
        //非同期処理確認のためのwait
        while (true) {
            if (workPromise.isDone()) {
                break;
            }
            Thread.sleep(100);
        }
    }

    // 処理結果
    //Start
    //Called
    // 実行スレッド名:jobs-thread-1
    // Working...
    // Done!
    // Finished
    //End
    //実行にかかった時間:3028 ミリ秒

複数処理時のパフォーマンス

同期処理・直列呼び出しの場合

   @Test
    public void testDoJobSequential() throws Exception {
        new Work().doWork();
        new Work().doWork();
        new Work().doWork();
        System.out.println("Called");
    }

    // 処理結果
    //Start
    // 実行スレッド名:main
    // Working...
    // Done!
    // 実行スレッド名:main
    // Working...
    // Done!
    // 実行スレッド名:main
    // Working...
    // Done!
    //Called
    //End
    //実行にかかった時間:9004 ミリ秒

複数処理時のパフォーマンス

非同期処理・並列呼び出しの場合

    @Test
    public void testDoJobParallel() throws Exception {
        //処理は非同期で開始されている
        final Promise<Work> workPromise1 = Work.doJobAsync();
        final Promise<Work> workPromise2 = Work.doJobAsync();
        final Promise<Work> workPromise3 = Work.doJobAsync();
        System.out.println("Called");
        
        final F.Promise<List<Work>> promises = F.Promise.waitAll(workPromise1,
                                                                 workPromise2,
                                                                 workPromise3);

        //非同期処理確認のためのwait
        while (true) {
            if (promises.isDone()) {
                break;
            }
            Thread.sleep(100);
        }
    }

    // 処理結果
    //Start
    //Called
    // 実行スレッド名:jobs-thread-1
    // Working...
    // 実行スレッド名:jobs-thread-2
    // Working...
    // 実行スレッド名:jobs-thread-3
    // Working...
    // Done!
    // Done!
    // Done!
    //End
    //実行にかかった時間:3139 ミリ秒

Playでの利用場面

PlayFramework : HTTP による非同期プログラミング

  • 複数の外部API呼び出しの並列化
  • CSV出力など処理コストがかかるブロッキングIOを回避
  • CPUコストがかかる処理を並列処理化(本格的ならAkkaを使う)

参考

  • PlayExcelモジュール
  • Excel生成処理遅延時のスレッドブロックを回避
  • controllers.ExcelControllerHelper
  • play.modules.excel.RenderExcel
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment