Java concurrency in_practice_chap06

36
Java並行処理プログラミング その「基盤」と「最新API」を究めるJava Concurrency in Practice 社内勉強会資料 6タスクの実行 大槌剛彦 (@ohtsuchi)

Transcript of Java concurrency in_practice_chap06

Page 1: Java concurrency in_practice_chap06

Java並行処理プログラミング

―その「基盤」と「最新API」を究める―

Java Concurrency in Practice

社内勉強会資料

第6章 タスクの実行

大槌剛彦 (@ohtsuchi)

Page 2: Java concurrency in_practice_chap06

6章の内容

•(6-1) 2つの方式の紹介とその欠点

・sequential approach

poor responsiveness and throughput

・thread per task approach

poor resource management

•(6-2) Executor Framework –実行ポリシー

–スレッドプール

–Executorのライフサイクル

–タスクの遅延開始と周期的実行

・(6-3)並列化できる箇所/すべき箇所を見つける –ページレンダラの例

–Callable and Future

–異質なタスクを並列化する限界

–CompletionService

–タスクに時間制限を設ける

Page 3: Java concurrency in_practice_chap06

6-1. タスクをスレッドで実行する

•最初のステップ

–タスク境界(task boundaries)を見つけること

•タスク

–独立した活動

–他のタスクの結果に依存しない

サーバアプリケーションの場合

•個々の client リクエスト

–独立性

–適切なサイズ

Page 4: Java concurrency in_practice_chap06

6-1-1 タスクを逐次的に実行する

•List 6-1 Sequential Web Server.

–1度に1つのリクエストしか扱えない

–サーバが1つのリクエストを処理する間、新たな接続は待たされる

–スループットも応答性も得られない

–GUIフレームワークでは、single thread で sequentially なタスクに利点(9章)

class SingleThreadWebServer {

public static void main(String[] args) throws IOException {

ServerSocket socket = new ServerSocket(80);

while (true) {

Socket connection = socket.accept();

handleRequest(connection);

}

Page 5: Java concurrency in_practice_chap06

6-1-2. タスクのためのスレッドを明示的に作る

•List 6-2 Web Server that Starts a New Thread for Each Request.

–main loop が クライアントからの接続毎に new thread を作る •new thread がリクエストを処理

•main thread はリクエストを処理しない

class ThreadPerTaskWebServer {

public static void main(String[] args) throws IOException {

ServerSocket socket = new ServerSocket(80);

while (true) {

final Socket connection = socket.accept();

Runnable task = new Runnable() {

public void run() {

handleRequest(connection);

}

};

new Thread(task).start();

}

Page 6: Java concurrency in_practice_chap06

6-1-2. タスクのためのスレッドを明示的に作る

•3つの結果

–応答性の向上※

•main はタスクの処理を行わないため。

•前の処理が完了する前に、新しい接続を受け付ける事ができるため。

–スループットの向上※

•プロセッサが複数あれば

•タスクを並列に処理できるため

–タスクを処理するコードはスレッドセーフである必要

•複数のタスクのために並行に起動するため

※負荷が重くない時、リクエストがサーバの処理能力を超えない時

Page 7: Java concurrency in_practice_chap06

6-1-3 制限のないスレッド作成の欠点

•スレッドのライフサイクルのオーバヘッド –スレッドの作成と廃棄はただではない

•リソースの消費 –特にメモリ。

–実行状態のスレッドの数がプロセッサの数より多いと、スレッドはidle状態になる

–idle状態のスレッドが多いと、メモリ消費量が増えて、garbage collector の負担が大きくなる

–全てのCPUがbusyなら、それ以上スレッドを作るのは意味が無いし、サーバの能力が劣化する

•安定性 –作れるスレッドの数にはlimitがある

–limit に達した場合、結果多くの場合、OutOfMemoryError

–OOMEからのリカバリはとてもrisky 。このlimitに達しないようなプログラムを構造する方がずっと容易。

この方式の問題点:スレッドを作る数に制限が無いこと

Page 8: Java concurrency in_practice_chap06

6-2 Executor フレームワーク

•5章(5-3 p102)で サイズ制限のあるキュー (bounded queues)

を使用した

–過負荷でメモリ不足にならないために

–Thread pool はそれと同じ考え方でスレッドを管理する

•java.util.concurrent パッケージは、Executor フレームワークの一部として、柔軟なThread poolの実装を提供

Page 9: Java concurrency in_practice_chap06

6-2 Executor フレームワーク

•List 6-3 Executor Interface.

•taskの submission(依頼) と execution(実行) を分離 –producer consumer pattern (5-3)

public interface Executor {

void execute(Runnable command);

}

Page 10: Java concurrency in_practice_chap06

6-2-1Executor を使ったWebサーバ

• List 6-4. Web Server Using a Thread Pool.

class TaskExecutionWebServer {

private static final int NTHREADS = 100;

private static final Executor exec

= Executors.newFixedThreadPool(NTHREADS);

public static void main(String[] args) throws IOException {

ServerSocket socket = new ServerSocket(80);

while (true) {

final Socket connection = socket.accept();

Runnable task = new Runnable() {

public void run() {

handleRequest(connection);

}

};

exec.execute(task);

}

Page 11: Java concurrency in_practice_chap06

6-2-1Executor を使ったWebサーバ

•thread per task approach の動作にするExecutorの例 –List 6-5

•sequential approach の動作にするExecutorの例 –List 6-6

public class ThreadPerTaskExecutor implements Executor {

public void execute(Runnable r) {

new Thread(r).start();

};

}

public class WithinThreadExecutor implements Executor {

public void execute(Runnable r) {

r.run();

};

}

Page 12: Java concurrency in_practice_chap06

6-2-2 実行ポリシー

•タスクの実行の what, where, when, how –タスクを

•どのスレッドで実行するか?

•どんな順序で実行するか?(FIFO, LIFO, priority order)

•並行に実行する数は?

•キューに並ばす最大数は?

•過負荷でrejectしなければいけない時にどのタスクを犠牲にするか? アプリケーションの通知はどうするか?

•実行前後でどんなアクションを行うべきか?

•実行ポリシーの仕様を、タスクの依頼から分離 –実働機 のリソース状況にマッチした実行ポリシーを指定することが困難ではなくなる。

Page 13: Java concurrency in_practice_chap06

6-2-3 スレッドプール

•worker threads の均質な(一種類のスレッドから成る)プールを管理

•利点

–既存の thread の再利用

–thread 作成の待ち時間がタスクの実行を遅らせない→応答性向上

–thread pool サイズの適切な tuning

•→ プロセッサを無駄に遊ばせない

•→ メモリ不足にならない

Page 14: Java concurrency in_practice_chap06

6-2-3 スレッドプール

Executors の static ファクトリメソッド

•newFixedThreadPool

•固定サイズ

•スレッドを最大プールサイズまで作る

•プールサイズを一定に保つ •return new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, new

LinkedBlockingQueue<Runnable>())

•newCachedThreadPool

•more flexibility

•idle threads は刈り取る

•要求が増えたら new threads を追加

•プールのサイズ制限が無い •return new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new

SynchronousQueue<Runnable>());

Page 15: Java concurrency in_practice_chap06

6-2-3 スレッドプール

Executors の static ファクトリメソッド

•newSingleThreadExecutor

•single worker thread

•task queue の順序(FIFO, LIFO, priority order)に応じて、 sequentially に処理する事が保証されている。

•return new FinalizableDelegatedExecutorService (new ThreadPoolExecutor(1, 1, 0L,

TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>()));

•newScheduledThreadPool

•固定サイズ

•遅延開始、周期的実行をサポート

•Timerに似ている (→ 6-2-5 参照) •return new ScheduledThreadPoolExecutor(corePoolSize);

→ThreadPoolExecutorの構成については第8章参照

Page 16: Java concurrency in_practice_chap06

6-2-4 Executor のライフサイクル

•graceful shutdown

•開始した仕事は無事に終わるが、新たな仕事は受け付けない

•abrupt shutdown

•唐突なshutdown。電源OFF。

•ExecutorService

•Executor を extends した interface

•ライフサイクルを管理するメソッドが定義されている

•List 6-7. Lifecycle Methods in ExecutorService.

public interface ExecutorService extends Executor {

void shutdown();

List<Runnable> shutdownNow();

boolean isShutdown();

boolean isTerminated();

boolean awaitTermination(long timeout, TimeUnit unit)

throws InterruptedException;

Page 17: Java concurrency in_practice_chap06

6-2-4 Executor のライフサイクル ExecutorService が暗黙的に定義しているライフサイクル

•ExecutorService の ライフサイクルの3つのステート

•running

•shutting down

•terminated

•shutdown メソッド

•graceful shutdown

•新たなタスクは受け付けない

•前に依頼されたタスク(実行を開始していないものも含めて)は完了可能

→ 終了するまで待つ

•shutdownNow メソッド

•abrupt shutdown

•実行中のタスクのキャンセルを試みる

•キューにあってまだ始まっていないタスクはスタートしない

Page 18: Java concurrency in_practice_chap06

6-2-4 Executor のライフサイクル

•シャットダウンの後に依頼されたタスク

•rejected execution handler (8-3-3)が取り扱う

•黙ってタスクを捨てる

or

•RejectedExecutionException をthrowする

•全てのタスクが完了したら

→ ExecutorService は terminated ステートに遷移

•awaitTermination メソッド

•ExecutorService が terminated ステートへ到達するのを待つ •(apiリファレンス) 「シャットダウン要求後にすべてのタスクが実行を完了していたか、タイムアウトが発生するか、現在のスレッドで割り込みが発生するか、そのいずれかが最初に発生するまでブロックします」

•isTerminated メソッド

•ExecutorService が terminated ステートへ到達したか否かをチェック

•(apiリファレンス)「シャットダウンに続いてすべてのタスクが完了していた場合、true を返します。 」

•shutdown に続いて awaitTermination を呼ぶのはよく行われる

→ シャットダウンに関しては第7章で

Page 19: Java concurrency in_practice_chap06

6-2-4 Executor のライフサイクル •ライフサイクルのサポートを加えた Webサーバの例

•List 6-8. Web Server with Shutdown Support.

class LifecycleWebServer {

private final ExecutorService exec = ...;

public void start() throws IOException {

ServerSocket socket = new ServerSocket(80);

while (!exec.isShutdown()) {

try {

final Socket conn = socket.accept();

exec.execute(new Runnable() {

public void run() { handleRequest(conn);

});

} catch (RejectedExecutionException e) {

if (!exec.isShutdown())

log("task submission rejected", e);

}

}

}

public void stop() { exec.shutdown(); }

void handleRequest(Socket connection) {

Request req = readRequest(connection);

if (isShutdownRequest(req))

stop();

else

dispatchRequest(req);

}

Page 20: Java concurrency in_practice_chap06

6-2-5 タスクの遅延開始と周期的実行

•(×)java.util.Timer にはいくつか欠点がある

→ (○)ScheduledThreadPoolExecutor を代わりに使うように。

•Timerの欠点

•1つのTimerで1つのスレッド。

•複数のTimerTaskを実行する時

→ 1つの TimerTask の実行が長すぎると

→ 他のタスクが不正確になる

※ ScheduledThreadPoolExecutor は複数スレッドを使って回避する

•システムクロックの変更に影響を受けてしまう

•TimerTask が非チェック例外をthrowした時の対応がお粗末

•Timer スレッドがcatchしない

•Timer スレッドが終了してしまう。Timer全体がキャンセルされたと誤判断

•"thread leakage" と呼ばれる問題

•(スケジューリング済みで未実行のタスクが実行されない、新たなタスクはスケジュールされない)

•→ 7-3 で避ける方法を説明

Page 21: Java concurrency in_practice_chap06

6-2-5 タスクの遅延開始と周期的実行

•List 6-9 Timer の動作をデモするクラス

•DelayQueue

•scheduling service を自作する場合に使える

•BlockingQueue の実装クラス

•ScheduledThreadPoolExecutorのようなスケジューリング機能

public class OutOfTime {

public static void main(String[] args) throws Exception {

Timer timer = new Timer();

timer.schedule(new ThrowTask(), 1);

SECONDS.sleep(1);

timer.schedule(new ThrowTask(), 1); // “Timer already cancelled” SECONDS.sleep(5);

}

static class ThrowTask extends TimerTask {

public void run() { throw new RuntimeException(); }

}

Page 22: Java concurrency in_practice_chap06

6-3 並列化できる箇所/すべき箇所を見つける

•タスク境界(task boundary)が自明な場合

•server applications の 1つのclient request •1つのclient requestの中にさらに並列できる部分がある (DBサーバなど)

•6-3-1 Example: Sequential Page Renderer

•一番単純

•長時間待たされる

Page 23: Java concurrency in_practice_chap06

6-3-2 結果を返すタスク : Callable and Future

•Runnable run メソッド

•値を返せない

•チェックされる例外をthrowできない

•Callable call メソッド

•値を返せる

•チェックされる例外をthrowできる

•Executor が実行するタスクのlifecycle

•4つの段階:

•created

•submitted

•started

•completed

•Executor framework でのタスクのキャンセル (※キャンセルについては第7章で)

•submit されたけれど、started になってないタスク → いつでもキャンセル可能

•started のタスク → interruption に応答する場合キャンセル可能

•completed のタスク → 何も起きない

Page 24: Java concurrency in_practice_chap06

6-3-2 結果を返すタスク : Callable and Future

•Future

•タスクのlifecycleを表現

•前に進めるだけ、後戻りはできない

•ExecutorService のlifecycleと同様

•調べるメソッド

•タスクが completed かどうか

•タスクが cancel されたかどうか

•タスクの結果を取り出すメソッド

•List 6-11 Callable and Future Interfaces.

public interface Callable<V> {

V call() throws Exception;

}

public interface Future<V> {

boolean cancel(boolean mayInterruptIfRunning);

boolean isCancelled();

boolean isDone();

V get() throws InterruptedException, ExecutionException,

CancellationException;

V get(long timeout, TimeUnit unit)

throws InterruptedException, ExecutionException,

CancellationException, TimeoutException;

Page 25: Java concurrency in_practice_chap06

6-3-2 結果を返すタスク : Callable and Future

•Future.get の振る舞い

•タスクのstateによって異なる

•completed

•直ちにreturnする or

•Exceptionをthrow

•完了していなければ、完了するまでブロック

•Exceptionをthrowして完了の場合

•get は ExecutionException でラップしてrethrowする

→ getCause メソッドで元の例外を取得できる

•cancelled

•get は CancellationException を throwする

Page 26: Java concurrency in_practice_chap06

6-3-2 結果を返すタスク : Callable and Future

•Future を作成する方法

•ExecutorService の submit メソッド

•FutureTask のインスタンスを明示的に作成

•AbstractExecutorService の newTaskFor メソッド

•defaultの実装は FutureTask をnewしてるだけ

•overrideする事でFuture のインスタンスの作り方を制御できる(java6から)

•List 6-12 Default Implementation of newTaskFor in ThreadPoolExecutor.

(参考: クラス図) http://blog.thihy.info/wp-content/uploads/2013/01/%E7%BA%BF%E7%A8%8B%E6%B1%A0%E7%BB%A7%E6%89%BF%E5%85%B3%E7%B3%BB.jpg

(参考: FutureTaskクラスがJava SE 6で変更)

http://itpro.nikkeibp.co.jp/article/COLUMN/20071001/283396/

protected <T> RunnableFuture<T> newTaskFor(Callable<T> task) {

return new FutureTask<T>(task);

}

Page 27: Java concurrency in_practice_chap06

6-3-3 例: Future を使うページレンダラ •2つのタスクに分割

•テキストのrender

•画像のダウンロード → Callable を作って → ExecutorService に submit

•List 6-13 Waiting for Image Download with Future.

public class FutureRenderer {

private final ExecutorService executor = ...;

void renderPage(CharSequence source) {

final List<ImageInfo> imageInfos = scanForImageInfo(source);

Callable<List<ImageData>> task =

new Callable<List<ImageData>>() {

public List<ImageData> call() {

List<ImageData> result

= new ArrayList<ImageData>();

for (ImageInfo imageInfo : imageInfos)

result.add(imageInfo.downloadImage());

return result;

}

};

Future<List<ImageData>> future = executor.submit(task);

renderText(source);

try {

List<ImageData> imageData = future.get();

for (ImageData data : imageData)

renderImage(data);

} catch (InterruptedException e) {

// thread の interrupted status を再確立する

Thread.currentThread().interrupt();

// 結果はいらないので、タスクをcancel

future.cancel(true);

} catch (ExecutionException e) {

throw launderThrowable(e.getCause());

Page 28: Java concurrency in_practice_chap06

6-3-4 異質なタスクを並列化する限界

前の例(FutureRenderer) では

•テキスト処理の時間 < 画像処理の時間

•実行性能は逐次処理とあまり変わらない

•しかし、コードは逐次処理よりも複雑に

→ 苦労の成果にも限界

→ 成果が上がるのは同質のタスクの場合

Page 29: Java concurrency in_practice_chap06

6-3-5 CompletionService:

Executor Meets BlockingQueue •複数タスクの完了

•タイムアウトが0の Future.get で何度もポーリング

↓ もっと良い方法

•CompletionService を使う

•take や poll メソッドが使える

•ExecutorCompletionService

•CompletionService の実装クラス

Page 30: Java concurrency in_practice_chap06

6-3-5 CompletionService:

•ExecutorCompletionService のソースの抜粋

public class ExecutorCompletionService<V> implements CompletionService<V> {

private final Executor executor;

private final AbstractExecutorService aes;

private final BlockingQueue<Future<V>> completionQueue;

private class QueueingFuture extends FutureTask<Void> {

QueueingFuture(RunnableFuture<V> task) {

super(task, null);

this.task = task;

}

protected void done() { completionQueue.add(task); } // FutureTask にはtaskが完了したら呼ばれるdoneメソッドがある

private final Future<V> task;

}

public ExecutorCompletionService(Executor executor) {

if (executor == null)

throw new NullPointerException();

this.executor = executor;

public Future<V> submit(Callable<V> task) {

if (task == null) throw new NullPointerException();

RunnableFuture<V> f = newTaskFor(task); // return new FutureTask<V>(task);

// taskの実行をexecutorに委譲 // taskが依頼されるとtaskはQueueingFutureでラップされる

executor.execute(new QueueingFuture(f));

return f;

}

public Future<V> take() throws InterruptedException {

return completionQueue.take();

}

public Future<V> poll() {

return completionQueue.poll();

}

Page 31: Java concurrency in_practice_chap06

6-3-6 CompletionService を使ったページレンダラ •List 6-15

•画像1つ1つのダウンロード → 別々のタスク → スレッドプールで実行

public class Renderer {

private final ExecutorService executor;

Renderer(ExecutorService executor) { this.executor = executor; }

void renderPage(CharSequence source) {

final List<ImageInfo> info = scanForImageInfo(source);

CompletionService<ImageData> completionService =

new ExecutorCompletionService<ImageData>(executor);

for (final ImageInfo imageInfo : info)

completionService.submit(new Callable<ImageData>() {

public ImageData call() {

return imageInfo.downloadImage();

}

});

renderText(source);

try {

for (int t = 0, n = info.size(); t < n; t++) {

// takeで取り出す順番は、タスクが完了した順番 // 何も存在しない場合は待機

Future<ImageData> f = completionService.take();

ImageData imageData = f.get();

renderImage(imageData);

}

Page 32: Java concurrency in_practice_chap06

6-3-7 タスクに時間制限を設ける

•例: 広告配信

•ADサーバから2秒以内に取り込めなかったら

•→ デフォルトの広告を表示

•→ 広告の取り込みは中断

•時間指定付きの Future.get

•結果が得られば直ちにreturnする

•タイムアウトまで得られなければ TimeoutException を throw

•get に渡すタイムアウトが負数の場合は0扱い

•タスクのキャンセル

•TimeoutException を catch → future.cancel(true);

→ List 6-13, 6-16 で使用

※第7章

Page 33: Java concurrency in_practice_chap06

6-3-7 タスクに時間制限を設ける •List 6-16 Fetching an Advertisement with a Time Budget.

•広告を取り込むタスク → Executorに依頼

Page renderPageWithAd() throws InterruptedException {

long endNanos = System.nanoTime() + TIME_BUDGET;

Future<Ad> f = exec.submit(new FetchAdTask());

// ページを Render しながら 広告を待つ

Page page = renderPageBody();

Ad ad;

try {

// 一定の時間(TIME_BUDGET)だけ待つ

long timeLeft = endNanos - System.nanoTime();

ad = f.get(timeLeft, NANOSECONDS);

} catch (ExecutionException e) {

ad = DEFAULT_AD;

} catch (TimeoutException e) {

ad = DEFAULT_AD;

f.cancel(true);

}

page.setAd(ad);

return page;

Page 34: Java concurrency in_practice_chap06

6-3-8 例: 旅行予約ポータル

•例: 複数企業からビッドを取り込む

•一定時間内の応答だけ提示

•タスク境界

•1つの起業からのビッド → 独立している → 妥当なタスク境界

•複数企業の数分、タスクをn個

•スレッドプールに依頼

•複数のFutureで管理

•時間指定付きの Future.get

•結果を逐次的に取り込む

↓もっと簡単な方法

•時間指定付きの invokeAll

Page 35: Java concurrency in_practice_chap06

6-3-8 例: 旅行予約ポータル

•invokeAll

•引数 : a collection of tasks

•戻り値: a collection of Futures

•この2つのcollectionは構造が同じ。

•引数のタスクのcollectionのiteratorの順番で戻り値のcollectionにFutureが加わる

•時間指定付きの invokeAll がリターンするのは

•全てのタスクが完了したとき

•呼び出しているスレッドがinterruptされた時

•タイムアウトになった時

→ 各タスクは

•正常に完了している

or

•キャンセルされている 、かのどちらか。

→ get または isCancelled を読んでどちらの結果なのか判別できる

Page 36: Java concurrency in_practice_chap06

6-3-8 例: 旅行予約ポータル

•List 6-17 時間制限付きで旅行の見積もりをリクエストする (抜粋)

private class QuoteTask implements Callable<TravelQuote> {

public List<TravelQuote> getRankedTravelQuotes( …) {

List<QuoteTask> tasks = new ArrayList<QuoteTask>();

for (TravelCompany company : companies)

tasks.add(new QuoteTask(company, travelInfo));

List<Future<TravelQuote>> futures =

exec.invokeAll(tasks, time, unit);

List<TravelQuote> quotes =

new ArrayList<TravelQuote>(tasks.size());

Iterator<QuoteTask> taskIter = tasks.iterator();

for (Future<TravelQuote> f : futures) {

QuoteTask task = taskIter.next();

try {

quotes.add(f.get());

} catch (ExecutionException e) {

quotes.add(task.getFailureQuote(e.getCause()));

} catch (CancellationException e) {

quotes.add(task.getTimeoutQuote(e));

}

}

Collections.sort(quotes, ranking);

return quotes;