ExecutorService の復習

Android で非同期処理っていうと、真っ先に AsyncTask が出てくるんですが、なるべくなら Java 標準のマルチスレッドAPI である ExecutorService を使った方が良いと思ってます。

Android で初めて Java を書いたので細かい仕様がよく分からず、勉強がてら動きを確認してみました。

1. 1つのワーカスレッドに実行させる

newSingleThreadExecutor でワーカスレッドを一つ持つ Executor を生成して、2つのタスク(=非同期で実行させる処理)を順に実行。

public void singleThreadExecutorBasicTest() throws Exception {
	final ExecutorService executor = Executors.newSingleThreadExecutor();
	
	Log.d(TAG, "Primary ThreadID:" + Thread.currentThread().getId());
	executor.submit(new Runnable() {
		@Override
		public void run() {
			Log.d(TAG, "Run task A. ThreadId:" + Thread.currentThread().getId());
		}
	});

	executor.submit(new Runnable() {
		@Override
		public void run() {
			Log.d(TAG, "Run task B. ThreadId:" + Thread.currentThread().getId());
		}
	});
}

出力

08-29 17:18:12.891: D/ExecutorTest(391): Primary ThreadID:4943

08-29 17:18:12.891: D/ExecutorTest(391): Run task A. ThreadId:4944
08-29 17:18:12.891: D/ExecutorTest(391): Run task B. ThreadId:4944

A→B の順で(=submit した順で)実行される。 Primary と task で ThreadID が異なる、2つの task は同じ ThreadID であることに注目。

2. 最初のタスクに時間がかかったら?

タスクA の実行に時間がかかる場合、タスクB はどうなる?

public void singleThreadExecutorHeavyWorkTest() throws Exception {
	final ExecutorService executor = Executors.newSingleThreadExecutor();
	
	Log.d(TAG, "Primary ThreadID:" + Thread.currentThread().getId());

	executor.submit(new Runnable() {
		@Override
		public void run() {
			Log.d(TAG, "Run task A start. ThreadId:" + Thread.currentThread().getId());
			
			// Wait
			try { Thread.sleep(3000); } catch (InterruptedException e) { } 
			
			Log.d(TAG, "Run task A end.");
		}
	});

	executor.submit(new Runnable() {
		@Override
		public void run() {
			Log.d(TAG, "Run task B. ThreadId:" + Thread.currentThread().getId());
		}
	});
}

出力

08-29 17:22:04.288: D/ExecutorTest(1511): Primary ThreadID:5063

08-29 17:22:04.288: D/ExecutorTest(1511): Run task A start. ThreadId:5064
08-29 17:22:07.291: D/ExecutorTest(1511): Run task A end.
08-29 17:22:07.291: D/ExecutorTest(1511): Run task B. ThreadId:5064

A→B の順で実行される。シングルスレッドなので、並列に処理されることはない。

3. ThreadPoolExecutor を使ったら?

Executors.newSingleThreadExecutor() の代わりに Executors.newFixedThreadPool(2) としてみる。これによりワーカスレッドを2つ使う Execurot が生成される。

public void threadPoolExecutorHeavyWorkTest() throws Exception {
	final ExecutorService executor = Executors.newFixedThreadPool(2);
	
	Log.d(TAG, "Primary ThreadID:" + Thread.currentThread().getId());

	executor.submit(new Runnable() {
		@Override
		public void run() {
			Log.d(TAG, "Run task A start. ThreadId:" + Thread.currentThread().getId());
			
			// Wait
			try { Thread.sleep(3000); } catch (InterruptedException e) { } 
			
			Log.d(TAG, "Run task A end.");
		}
	});

	executor.submit(new Runnable() {
		@Override
		public void run() {
			Log.d(TAG, "Run task B. ThreadId:" + Thread.currentThread().getId());
		}
	});
}

出力

08-29 17:29:01.731: D/ExecutorTest(3017): Primary ThreadID:5255

08-29 17:29:01.731: D/ExecutorTest(3017): Run task A start. ThreadId:5256
08-29 17:29:01.731: D/ExecutorTest(3017): Run task B. ThreadId:5257
08-29 17:29:04.725: D/ExecutorTest(3017): Run task A end.

タスクA と タスクB で ThreadID が異なる事に注目。 スレッドが2つ使えるので、タスクA の終了を待たずにタスクB が実行される。というか、タスクA から始まる保証もない。

3. SingleThreadScheduledExecutor を使ってみる

話題を変えて、タスクの実行時間を制御できる Scheduled系 の Executor を使ってみる。

public void singleThreadScheduledExecutorBasicTest() throws Exception {
	final ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor();

	Log.d(TAG, "Primary ThreadID:" + Thread.currentThread().getId());
	executor.schedule(new Runnable() {
		@Override
		public void run() {
			Log.d(TAG, "Run task A. ThreadId:" + Thread.currentThread().getId());
		}
	}, 5, TimeUnit.SECONDS);

	executor.schedule(new Runnable() {
		@Override
		public void run() {
			Log.d(TAG, "Run task B. ThreadId:" + Thread.currentThread().getId());
		}
	}, 3, TimeUnit.SECONDS);
}

出力

08-29 17:36:16.125: D/ExecutorTest(3547): Primary ThreadID:5285

08-29 17:36:19.128: D/ExecutorTest(3547): Run task B. ThreadId:5286
08-29 17:36:21.130: D/ExecutorTest(3547): Run task A. ThreadId:5286

開始から3秒後にタスクBが、開始から5秒後にタスクAが実行される。 submit した順は関係ないことに注意。

4. スケジュールされた時間に別のタスクが実行中だったら?

3秒後に実行されるタスクBの処理が終わらない時、5秒後に実行されるタスクAはどうなる?

public void singleThreadScheduledExecutorHeavyWorkTest() throws Exception {
	final ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor();
	Log.d(TAG, "Primary ThreadID:" + Thread.currentThread().getId());
	executor.schedule(new Runnable() {
		@Override
		public void run() {
			Log.d(TAG, "Run task A. ThreadId:" + Thread.currentThread().getId());
		}
	}, 5, TimeUnit.SECONDS);

	executor.schedule(new Runnable() {
		@Override
		public void run() {
			Log.d(TAG, "Run task B start. ThreadId:" + Thread.currentThread().getId());
			
			// Wait
			try { Thread.sleep(10000); } catch (InterruptedException e) { } 
			
			Log.d(TAG, "Run task B end.");
		}
	}, 3, TimeUnit.SECONDS);
}

出力

08-29 17:44:18.139: D/ExecutorTest(4737): Primary ThreadID:5456

08-29 17:44:21.142: D/ExecutorTest(4737): Run task B start. ThreadId:5457
08-29 17:44:31.143: D/ExecutorTest(4737): Run task B end.
08-29 17:44:31.143: D/ExecutorTest(4737): Run task A. ThreadId:5457

5秒後とスケジュールされたタスクAだが、タスクB が終わるまで待たされる。シングルスレッドなので。

5. そこで ScheduledThreadPoolExecutor を使ってみる

Executors.newSingleThreadScheduledExecutor() の代わりに Executors.newScheduledThreadPool(2) としてみる。

public void threadPoolScheduledExecutorHeavyWorkTest() throws Exception {
	final ScheduledExecutorService executor = Executors.newScheduledThreadPool(2);
	Log.d(TAG, "Primary ThreadID:" + Thread.currentThread().getId());
	executor.schedule(new Runnable() {
		@Override
		public void run() {
			Log.d(TAG, "Run task A. ThreadId:" + Thread.currentThread().getId());
		}
	}, 5, TimeUnit.SECONDS);

	executor.schedule(new Runnable() {
		@Override
		public void run() {
			Log.d(TAG, "Run task B start. ThreadId:" + Thread.currentThread().getId());
			
			// Wait
			try { Thread.sleep(10000); } catch (InterruptedException e) { } 
			
			Log.d(TAG, "Run task B end.");
		}
	}, 3, TimeUnit.SECONDS);
}

08-29 17:48:28.536: D/ExecutorTest(5439): Primary ThreadID:5558

08-29 17:48:31.550: D/ExecutorTest(5439): Run task B start. ThreadId:5559
08-29 17:48:33.542: D/ExecutorTest(5439): Run task A. ThreadId:5560
08-29 17:48:41.550: D/ExecutorTest(5439): Run task B end.

スレッドが2つ使えるので、タスクA は、スケジュール通り(タスクBの終了を待たずに submit してから5秒後に実行される。

6. Scheduled系Executor の Timer 的機能を使ってみる

schedule() は、一発だけ(Javascript の setTimeout みたいな)、繰り返し処理するには、scheduleAtFixedRatescheduleWithFixedDelay を使う。 まずは scheduleAtFixedRate から。

public void singleThreadScheduledExecutorTimerBasicTest() throws Exception {
	final ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor();

	Log.d(TAG, "Primary ThreadID:" + Thread.currentThread().getId());
	executor. scheduleAtFixedRate(new Runnable() {
		@Override
		public void run() {
			Log.d(TAG, "Run task A. ThreadId:" + Thread.currentThread().getId());
		}
	}, 5, 3, TimeUnit.SECONDS);
}

出力

08-29 17:59:33.556: D/ExecutorTest(7228): Primary ThreadID:5795

08-29 17:59:38.561: D/ExecutorTest(7228): Run task A. ThreadId:5796
08-29 17:59:41.565: D/ExecutorTest(7228): Run task A. ThreadId:5796
08-29 17:59:44.568: D/ExecutorTest(7228): Run task A. ThreadId:5796

最初は5秒、その後は3秒毎にタスクAが実行される。

7. 繰り返し実行されるタスクが重かったら?

繰り返しは3秒だけど、タスクAの実行に10秒かかったら、どうなる?

public void singleThreadScheduledExecutorTimerHeavyTest() throws Exception {
	final ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor();

	Log.d(TAG, "Primary ThreadID:" + Thread.currentThread().getId());
	executor.scheduleAtFixedRate(new Runnable() {
		@Override
		public void run() {
			Log.d(TAG, "Run task A. ThreadId:" + Thread.currentThread().getId());
		}
	}, 5, 3, TimeUnit.SECONDS);
}

出力

08-29 18:15:49.397: D/ExecutorTest(10157): Primary ThreadID:6188

08-29 18:15:54.413: D/ExecutorTest(10157): Run task A start. ThreadId:6189
08-29 18:16:04.403: D/ExecutorTest(10157): Run task A end.
08-29 18:16:04.403: D/ExecutorTest(10157): Run task A start. ThreadId:6189
08-29 18:16:14.404: D/ExecutorTest(10157): Run task A end.
08-29 18:16:14.404: D/ExecutorTest(10157): Run task A start. ThreadId:6189
08-29 18:16:24.405: D/ExecutorTest(10157): Run task A end.

3秒置きに設定しているが、タスクAが終わらないので、終わったらすぐに、次のタスクを実行する。

8. scheduleWithFixedDelay ではどうなる?

scheduleAtFixedRate の代わりに scheduleWithFixedDelay にしてみた。

public void singleThreadScheduledExecutorFixedDelayHeavyTest() throws Exception {
	final ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor();

	Log.d(TAG, "Primary ThreadID:" + Thread.currentThread().getId());
	executor.scheduleWithFixedDelay(new Runnable() {
		@Override
		public void run() {
			Log.d(TAG, "Run task A. ThreadId:" + Thread.currentThread().getId());
		}
	}, 5, 3, TimeUnit.SECONDS);
}

出力

08-29 18:22:37.183: D/ExecutorTest(10663): Primary ThreadID:6245

08-29 18:22:42.178: D/ExecutorTest(10663): Run task A start. ThreadId:6246
08-29 18:22:52.179: D/ExecutorTest(10663): Run task A end.
08-29 18:22:55.182: D/ExecutorTest(10663): Run task A start. ThreadId:6246
08-29 18:23:05.182: D/ExecutorTest(10663): Run task A end.
08-29 18:23:08.176: D/ExecutorTest(10663): Run task A start. ThreadId:6246

タスクAが終わって、さらに3秒待って、次のタスクを実行する。 FixedDelay は、前回のタスクが終わってからn秒待つ。 FixedRate は、の終了を待たずにn秒置きに実行するが、終わってない場合は仕方がないので終わるまで待つ、という感じらしい。

9. ScheduledThreadPoolExecutor ではどうか?

ScheduledThreadPoolExecutor と scheduleAtFixedRate の組み合わせではどうか?

public void singleThreadScheduledExecutorFixedDelayHeavyTest() throws Exception {
	final ScheduledExecutorService executor = Executors.newScheduledThreadPool(2);

	Log.d(TAG, "Primary ThreadID:" + Thread.currentThread().getId());
	executor.scheduleAtFixedRate(new Runnable() {
		@Override
		public void run() {
			Log.d(TAG, "Run task A start. ThreadId:" + Thread.currentThread().getId());
			
			// Wait
			try { Thread.sleep(10000); } 
			catch (InterruptedException e) { Log.w(TAG, "Interrupted task A. ThreadId:" + Thread.currentThread().getId()); } 
			
			Log.d(TAG, "Run task A end.");
		}
	}, 5, 3, TimeUnit.SECONDS);
}

出力

08-29 19:09:39.707: D/ExecutorTest(14173): Primary ThreadID:6605

08-29 19:09:44.712: D/ExecutorTest(14173): Run task A start. ThreadId:6606
08-29 19:09:54.713: D/ExecutorTest(14173): Run task A end.
08-29 19:09:54.713: D/ExecutorTest(14173): Run task A start. ThreadId:6606
08-29 19:10:04.713: D/ExecutorTest(14173): Run task A end.
08-29 19:10:04.713: D/ExecutorTest(14173): Run task A start. ThreadId:6606
08-29 19:10:14.714: D/ExecutorTest(14173): Run task A end.
08-29 19:10:14.714: D/ExecutorTest(14173): Run task A start. ThreadId:6606
08-29 19:10:24.705: D/ExecutorTest(14173): Run task A end.
08-29 19:10:24.705: D/ExecutorTest(14173): Run task A start. ThreadId:6606
08-29 19:10:34.705: D/ExecutorTest(14173): Run task A end.

あれ?2つのスレッドを使ってくれない。を登録した時点でスレッドは決まってるということかな。

まとめ

「タスクが実行されるスレッド」を意識すればハマることはなさそう。

シングルスレッドの場合は、submit あるいは schedule されたタスクは、一つのスレッドで順次処理される。スレッドプールを使っている場合は、スレッドの数だけ並列処理される。 ただし、scheduleAtFixedRate など繰り返し処理では、登録時にスレッドが決まるので、タスクの実行に時間がかかっても並列処理されない。

scheduleAtFixedRatescheduleWithFixedDelay はタイマー的な動きをするが、タスクの処理に時間がかかる場合は、意図した時間間隔で実行されない。タイマーとして使いたければ、Executor を2つ用意し、一つはタイマー専用、もうひとつをタスク実行専用とした方が良さそう。

長くなってしまったので、タスクのキャンセルとか、Terminate 系は別の機会に。