Edited at

幸せな非同期処理ライフを満喫するための基礎から応用まで

More than 1 year has passed since last update.

クライアントアプリにとって、マルチスレッドプログラミングは避けては通れない重要な概念です。しかし、気をつけないとハマるポイントも多く、初めてクライアントアプリを学ぶ人たちからすると、複雑で難解な取っつきづらいものでもあります。ここでは、スレッドの基本から、効率的な使い方、また複雑化しやすいポイントをシンプルに実装するためのノウハウを見ていきます。


TL;DR


  1. スレッドの取り扱い方を知る

    Threadをそのまま使わず、AsyncTaskIntentService、時にThreadPoolExecutorを使ってスレッドの使い方を効率化する。

  2. 複雑な処理フローをシンプルに扱うためのフレームワークを導入する

    PromiseRxAndroidなどで、複雑化しやすいポイントを整理する。


スレッドの基本

スレッドといえば、ThreadクラスやRunnableクラスがベースにあります。以下のようにすれば、簡単にスレッドを立ち上げることが出来ます。


SimpleThread.java


public class SimpleThread {
public static void main(String[] args) {
System.out.println("メインスレッドだよ");
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("別のスレッドだよ");
}
}).start();
}
}

Threadの処理の実体はrun()に記述しますが、Threadを開始するにはstart()を用います。

簡単ですね。


Runnable と Callable

マルチスレッドでは、二種類の基本的なインタフェースが提供されています。その内の一つが、上の例にもあげたRunnableで、もう一つにCallableがあります。

Runnableは、run()メソッドを持ち、引数、返り値ともにありません。また、検査例外(必ず catch する例外)を投げることもできません。

一方で、Callableは、call()メソッドを持ち、引数はありませんが返り値が任意の型にできます。また、検査例外を投げることが出来るようになっています。

実際には、これらをうまく使い分けて、非同期処理を実行していくことになります。


複数のスレッドの生成

では、何かしらの処理のためにスレッドを沢山作るとしましょう。たとえば、10,000スレッドくらい。


SimpleThread.java


public class SimpleThread {
public static void main(String[] args) {
System.out.println("メインスレッドだよ");
for (int i = 0; i < 10000; i++) {
Runnable runnable = new MyRunnable();
new Thread(runnable).start();
}
}
}

class MyRunnable implements Runnable {
@Override
public void run() {
// 何かする
}
}


スレッドは終了後に再度使いまわすことが出来ない為、通常、非同期に処理をしたい時には都度新しくインスタンスを生成してスレッドを起動するようにします。スレッドのインスタンスを作成すると、そのスレッド専用のメモリ領域が割り当てられます(これは仮想マシンがしてくれます)。10000個のスレッドをたちあげたら、10000個のスレッドそれぞれのためのメモリ領域がドカッと確保されることになります。


この他、スレッド生成にあたってシステムコールを実行したり、リソースの確保を行ったりするため、他のオブジェクト生成に比べて処理にかかるコストが高いと言われています。それを10000回繰り返すのは非常に非効率です。


スレッドプール

幸いなことに、スレッドのインスタンスを使いまわすための仕組みがすでに存在します。これをスレッドプールと言い、Java ではThreadPoolExecutorというクラスに実装されています。

スレッドプールの仕組みでは、実行したいタスク(Runnable)をキューに溜めておき、そこからスレッドが順次タスクを取り出し続け、無くなるまでスレッドを起動したまま処理を続けるように動作します。これによって、スレッド自体は終了しないものの、あたかもスレッドが使いまわされているように見えます。

スレッドプールを生成するには、Executorsを使用するのが便利です。

// 10 個のスレッドインスタンスをプールしたもの

ExecutorService pool1 = Executors.newFixedThreadPool(10);

// 必要に応じてスレッドを新規作成しながらも、一定期間はスレッドの使い回しをするもの
ExecutorService pool2 = Executors.newCachedThreadPool();

Executorは渡されたRunnableを実行するためのインタフェースで、実装によって実行の仕方が変わります(非同期でなく同じスレッドで直接渡されたRunnablerun()を呼び出す実装もあり得る)。これのサブインタフェースとして、ExecutorServiceが定義されており、ThreadPoolExecutorExecutorServiceを実装したクラスです。

これらを使って、スレッドインスタンスをうまく使いまわしながら効率的に非同期処理が実行できるようになります。

さてここで、Android を始めとして一般的には、UI を持つような仕組みの中では、UI の操作は必ずメインスレッドで実行するようにしています。しかし、スレッドプールはあくまでスレッドインスタンスを使いまわすことしかしないため、このままでは非同期処理をした結果を受けて UI に何かしら変更を加えることができません。


Handler

Android では、スレッド間の(より正確に言えば、Looperが動作しているスレッドへの)通信をするためにHandlerがいます。別スレッドでHandlerを用いてMessageを投げ、それをメインスレッド上でHandlerからMessageを受け取れば、めでたく別のスレッドの実行結果を受けて UI にフィードバックすることができるようになります。


AsyncTask

この仕組を単純化し、スレッドプールと合わせて提供しているクラスがAsyncTaskです。Android OS のバージョンごと、スレッドが並列実行されるか順番に実行されるかの違いがありますが、概ね提供している機能は同じです。

AsyncTask#doInBackground()は別スレッドで実行されます。そしてその結果をHandler経由で受け取ってメインスレッドで実行されるのがAsyncTask#onPostExecute(Result)です。

余談ですが、AsyncTaskが持つスレッドプールの最小サイズは、CPU の数 + 1 で決まります(2.x では 5 個固定)。最も多くスレッドプールにインスタンスを生成した場合でも、CPU の数 * 2 + 1 個までになります(2.x では 128 個固定)。ここにそのコードが有ります。


IntentService

Handlerを用いた非同期処理の仕組みとしてもう一つ、IntentServiceがあります。Serviceという UI を持たないコンポーネントにたいしてIntentを渡すと、非同期処理が裏で実行され、Activityの世界とは完全に切り離されたところで動きます。画面を閉じても裏で処理を続けていて欲しい時に便利です。

こちらは、非同期処理のリクエストをHandlerで別のスレッド(HandlerThread)で実行するため、その性質上必ず非同期処理は順番に(Intentを投げた順に)1つずつ実行されます。


処理の順番を見える化する

さて、実際にアプリケーションを作っていくと、非同期処理の結果を受けて UI の更新をするだけでなく、次の非同期処理を実行したり、さらにその結果を受けて…などと言おうように、処理がチェーン状に連なることがあります。

具体的に実装しようとなると、AsyncTask#onPostExecute(Result)で次のAsyncTaskを起動して……

あるいは、AsyncTaskLoaderを使うなら、LoaderCallbacks#onLoadFinished()で再度LoaderManager#initLoader()して……

あまり見たくないですね(AsyncTaskのほうがいくらか綺麗に見える気がしますが)。

場合によっては、非同期処理の間に何らかのメインスレッドでの仕事があったりして、頻繁に同期非同期の処理が行ったり来たりすることも多いでしょう。そのような場合のコードをAsyncTaskAsyncTaskLoaderで書き連ねるのはどう考えてもメンテナンスできなくなりそうです。

そこで、Promiseという考え方を導入して、一連の処理の流れをチェイン状につなげて書けるようにしてみます。

いくつかライブラリ(android-promise, jdeferred, RxJava)がありますが、概ね以下のように書くことができます。


MyActivity.java


// andorid-promise で書いてみた
public class MyActivity extends Activity {

@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.my_activity);

Promise.with(this, Void.class).then(new Task<Void, Void>() {
@Override
public void run(Void in, NextTask<Void> next) {
// do something on main thread
next.run(null);
}
}).thenOnAsyncThread(new Task<Void, Result>() {
@Override
public void run(Void in, NextTask<Result> next) {
// do something on async thread(not main thread)
Result result = ...
next.run(result);
}
}).thenOnMainThread(new Task<Result, Void>() {
@Override
public void run(Result in, NextTask<Void> next) {
// do something on main thread with result input
next.run(null);
}
}).thenOnAsyncThread(new Task<Void, Result>() {
@Override
public void run(Void in, NextTask<Result> next) {
// do something on main thread
Result result = ...
next.run(result);
}
}).setCallback(new Callback<Result>() {
@Override
public void onSuccess(Result result) {
// all task done!
}

@Override
public void onFailure(Bundle result, Exception exp) {
// something went wrong
}
}).execute(null);
}

@Override
public void onDestroy() {
Promise.destroyWith(this);
super.onDestroy();
}
}


あれしてこれしてそれからあの仕事をして……という流れがコードと綺麗に一致します。RxJava はもっとリッチな機能を提供してくれますが、Promise のように使うこともできます。これで、非同期処理のコードがあちこちに散らばって追い切れなくなる状況が回避できそうです。


まとめ

今回、Threadを使わない理由として、スレッドプールを使わないとリソースを食うという話題をあげましたが、他にもスレッドの待ち合わせなど、ややこしくかつ正しく実装することが難しいものもあります。そういったものも含めて、Threadをそのまま使うことは避けていきたいものです。

そして、往々にして複雑化しがちな一連の処理の流れを、Promise の考え方でもってシンプルにまとめると、読みやすくメンテしやすいコードになりそうです。