PlayFramework1でのFuture/Promiseの説明をしようと思ったら大事になったでござる。
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の場合は処理中は他の処理がブロックされる
- JDBCアクセスや(NIOを利用しない)ファイル操作などがブロッキングIOの例
ノンブロッキングへの熱い情熱
-
Play
-
NettyラッパーによるWebフレームワーク
-
Node.jsよりも速いという噂
-
Netty
-
Java NIOラッパーによるネットワークフレームワーク
-
非同期イベント駆動型
-
Promise
-
java Futureの拡張、非同期+コールバック機能
-
Akka(Play2)
-
アクターモデルの非同期処理フレームワーク
Play はとても短いリクエストで動作することを意図しています。Play は HTTP コネクタによってキューイングされたリクエストを処理するために固定のスレッドプールを使用します。最適な結果を得るために、このスレッドプールは可能な限り小さくあるべきです。デフォルトのプールサイズを設定するための最適値として、典型的には プロセッサ数 + 1 を使用します。(PlayFramework : HTTP リクエストの中断)
- DEVモード:1
- PRODモード:CPUコア数 + 1
- play status から確認可能
参考
スレッドをブロックしないようにする
これは、もしリクエストの処理時間がとても長い場合 (例えば、長い計算を待つなど) に、リクエストがスレッドプールをブロックし、アプリケーションの応答性に不利益をもたらすことを意味します。もちろん、プールにより多くのスレッドを追加することもできますが、リソースを浪費する結果となるかも知れませんし、いずれにしてもプールのサイズは決して無限にはなりません。(PlayFramework1 : HTTP リクエストの中断)
非同期・並列化処理による処理パフォーマンスの向上
前述した三つの非同期リモートリクエストの例を実装する別の方法は、コールバックを使うことです。ここでは await(…) の呼び出しに、すべての promises が完了した時に実行されるコールバックである play.libs.F.Action の実装が含まれています。(PlayFramework1 : コールバック)
Futureとはまだ存在しない計算結果に対するプレースホルダのようなもの
future, promise, delay とは、プログラミング言語における並列処理のデザインパターン。何らかの処理を別のスレッドで処理させる際、その処理結果の取得を必要になるところまで後回しにする手法。処理をパイプライン化させる。(Wikipedia : Future)
JavaのFutureインターフェース
java.util.concurrent.Future
FutureはV型の値をラップして、getするとその値が取れる。ただ、Futureは「まだ計算おわてないよー」的な状態も表すので、getしたときにまだ内部の値がない時は値が計算されるまでblockする。cancelとかもできる。(愛と勇気と缶ビール : 個人的Java並行/非同期処理めも)
そのまま使うとブロッキングされる(同一スレッドで動作する)ので全然非同期ではない、能動的に別スレッド化する必要がある(大発見 : Futureの使い方とFutureTaskのワナ)
(PlayFrameworkでの)Promiseとは
Promise は Play による Future 型のカスタマイズです。実際のところ、 Promise は Future でもあるので、標準的な Future として使用することもできます。しかし、 Promise にはとても興味深い属性: onRedeem(…) を使って、期待する値が利用可能になるとできるだけ早く呼び出されるコールバックを登録する機能が備わっています。(PlayFramework1 : Javaによる関数プログラミング)
PlayではPromiseクラスにより簡単に非同期・並列処理+コールバックが実装できるようになっている
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 ミリ秒
PlayFramework : HTTP による非同期プログラミング
- 複数の外部API呼び出しの並列化
- CSV出力など処理コストがかかるブロッキングIOを回避
- CPUコストがかかる処理を並列処理化(本格的ならAkkaを使う)
- PlayExcelモジュール
- Excel生成処理遅延時のスレッドブロックを回避
- controllers.ExcelControllerHelper
- play.modules.excel.RenderExcel