Android で非同期処理っていうと、真っ先に AsyncTask
が出てくるんですが、なるべくなら Java 標準のマルチスレッドAPI である ExecutorService を使った方が良いと思ってます。
Android で初めて Java を書いたので細かい仕様がよく分からず、勉強がてら動きを確認してみました。
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 であることに注目。
タスク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 の順で実行される。シングルスレッドなので、並列に処理されることはない。
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 から始まる保証もない。
話題を変えて、タスクの実行時間を制御できる 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 した順は関係ないことに注意。
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 が終わるまで待たされる。シングルスレッドなので。
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秒後に実行される。
schedule()
は、一発だけ(Javascript の setTimeout
みたいな)、繰り返し処理するには、scheduleAtFixedRate
か scheduleWithFixedDelay
を使う。
まずは 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が実行される。
繰り返しは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が終わらないので、終わったらすぐに、次のタスクを実行する。
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秒置きに実行するが、終わってない場合は仕方がないので終わるまで待つ、という感じらしい。
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
など繰り返し処理では、登録時にスレッドが決まるので、タスクの実行に時間がかかっても並列処理されない。
scheduleAtFixedRate
や scheduleWithFixedDelay
はタイマー的な動きをするが、タスクの処理に時間がかかる場合は、意図した時間間隔で実行されない。タイマーとして使いたければ、Executor を2つ用意し、一つはタイマー専用、もうひとつをタスク実行専用とした方が良さそう。
長くなってしまったので、タスクのキャンセルとか、Terminate 系は別の機会に。