【Android】Executorを使用して独自の非同期処理を実装する(1)

非同期での画像処理関連を直したりUserStreamのイベントを処理するスレッドプールをAsyncTaskのスレッドプールと共有させたりしていたら、何だか処理が遅くなってしまいました。

と言うか、AsyncTaskのスレッドプールだけだと流石に厳しいって言うか、スレッドの増減が激しすぎてとんでもないことになっているので、画像処理は画像処理用のスレッドプールを用意してあげよう、と思い立ち、ついでにAsyncTaskじゃなくて画像処理専用のワーカースレッドを作るようにしよう、とも思ったので、作ります。(日本語が不自由)

Executorってなに?

Executorと言うか、Executorのいるjava.util.concurrentパッケージJava 1.5から追加されたパッケージであり、ThreadRunnable、synchronize / wait / notifyだけでどうにかするにはあまりにも辛かったマルチスレッド処理を簡便化するためのオブジェクトが集められています。

具体的にどう言ったものがあるのかを全部説明するとあまりにも面倒なのでこの辺あたりを読んでおけばいいんじゃないでしょうか。

じゃあその中でExecutorは何をしてるのって話なんですが、大まかに言うとこんな感じです。

  • スレッドの生成・管理(キャンセルetcも含む)
  • バックグラウンドで実行する内容へのキューイング(スレッドプールの管理・実行内容のスケジューリング)
  • バックグラウンドで実行した後の処理へのキューイング(継続渡しのサポート)

実際にはこれら全部をExecutorがやるわけではない…と言うか、別のjava.util.concurrentパッケージ内のクラスに委譲したり、Executorを実装するクラスに委譲したりするんですが、まぁ大体こんなことができるんだなーぐらいに思っておけばいいでしょう。

Executorの種類

Executorはあくまでインターフェースなので何かしら実装しないといけません。

とは言え1から自分で実装するのは面倒だよねってことで色々と実装済みのクラスが用意されています。

まずはExecutorを継承するサブインターフェースを紹介しておきましょう。

Executor
  └ExecutorService
    └ScheduledExecutorService

ExecutorServiceは、ExecutorからキューイングされたRunnable(タスク)の継続や、Executorのシャットダウン(タスクの新規登録を受け入れない)、強制終了などをサポートするためのメソッドが用意されています。って言うか、これらのメソッドを使わないならそもそもExecutorを使う理由がないのでは?って気がします。

ScheduledExecutorServiceはそれに加えて遅延実行や登録したタスクを定期的に実行するためのメソッドがくっついています。

実際にExecutorを実装しているクラスはこんな感じです。

Executor
  └ExecutorService
    |└AbstractExecutorService
    |  └ThreadPoolExecutor───┐
    └ScheduledExecutorService    |
      └ScheduledThreadPoolExecutor

とまぁ、見ての通りExecutorを直に実装しているクラスは用意されていません。ほとんどの場合、Executorを本気で1から自作するならExecutorServiceを実装するかAbstractExecutorServiceを継承するかになるでしょう。

ThreadPoolExecutorクラスは、その名の通り内部にスレッドプールを作っておき、それを上手く使ってタスクを処理してくれます。たまに「ExecutorServiceはスレッドプールを使用して云々」みたいな解説をしている記事が見受けられますが、ExecutorService自体はそんな機能持ってません。そもそもアイツはインターフェースだぞ。

ScheduledThreadPoolExecutorはScheduledExecutorServiceを実装し、なおかつThreadPoolExecutorを継承しています。まぁ大体想像がつくと思いますが、スレッドプールを使いながらタスクの遅延実行や定期実行を行うことが可能です。

また、これらのデフォルト実装クラスを簡単に作れるExecutorsと言うクラスもありますが、今回は説明を割愛します。

今回やりたいことは「非同期通信で画像をDLしてきて終わったら表示する」なので、ThreadPoolExecutorで十分でしょう。

タスクの作成と継続

早速ThreadPoolExecutorを作っていってもいいんですが、先にバックグラウンドで実行するタスクとその継続の話をしておきます。

基本的にはExecutor#execute(Runnable)に渡すRunnableがタスクです。返り値が欲しい人のためにCallable<V>と言うインターフェースも用意されています。

しかし、CallableはRunnableを実装していないのでexecuteで渡すことができません。そこでFutureTask<V>でラップします。

このFutureTask(厳密にはFutureTaskが実装しているFutureインターフェース)が継続をサポートするオブジェクトです。こんな風にして使います。

Callable<String> caller = new Callable<String>() {
    @Override
    public String call() {
        // TODO:なんか時間がかかる処理
        return "終わったよ";
    }
}

executor.execute(new FutureTask<String>(caller) {

    // コンストラクタで渡したRunnable or Callable(タスク)が完了すると
    // FutureTask#done()が呼び出される
    @Override
    public void done() {
        // Future#get()を呼び出すと
        // コンストラクタで渡したCallableの返り値
        // を取得できる
        System.out.println(get());
    }
    
});


// 上記の内容程度の処理ならこんな風にも記述できる
Runnable runner = new Runnable() {
    @Override
    public void run() {
        // TODO:なんか時間がかかる処理
    }
}

executor.execute(new FutureTask<String>(runner, "終わったよ") {
    @Override
    public void done() {
        // コンストラクタで渡した固定値を取得できる
        System.out.println(get());
    }
});


Executor#executeではなく、ExecutorService#submitを使用する方法もあります。

Callable<String> caller = new Callable<String>() {
    @Override
    public String call() {
        // TODO:なんか時間がかかる処理
        return "終わったよ";
    }
}

Future<String> future = executorService.submit(caller);
System.out.println(future.get());

Future#getを呼び出すとその呼び出し元のスレッドがブロックされるので、こちらの方法を使う場合はgetを呼び出すタイミングを間違えないようにしましょう。

継続とAndroidのUIスレッド

じゃあ今度こそThreadPoolExecutorを…といきたいところですが、最後に一つ。

今までの話はずーっとJavaのお話です。Androidでこれを使用する場合、もう一つ考慮しなきゃいけないことがあります。それは「継続処理をUIスレッドで行うか否か」です。

UIスレッドと関係ない、バックグラウンドだけで完結する処理を行うなら上記の例の通り適当にFutureTaskを作ってしまえばいいんですが、バックグラウンドで処理した結果をUIスレッドに通知して何かしたい場合はどうしたってHandlerの力を借りなくてはなりません。

今回はDLしてきた画像(Bitmap)を自作したImageViewにセットする必要があるので、当然これは考えなければならない問題です。

たまに勘違いしている人がいる(最近はそうでもないのかな)んですが、Handlerはあくまでスレッド間の通知を行うためのものであって、確実にUIスレッドへ通知するための仕組みではありません。

上記の記事にも書かれていますが、Handler(Looper)を使うことでどのスレッドに通信するかをこちらから指定することができます。今回で言えば「確実にUIスレッドのLooperを持っているHandler」を手に入れなくてはなりません。

何だか大げさに言ってしまいましたが、Looper#getMainLooper()メソッドを呼び出せばUIスレッドのLooperは簡単に手に入るので大した問題ではないです。とは言え、Executorを作って、Callableを作って、Futureを作って、Handlerを作って、さらにそのHandlerの中で行うコールバックも作って…と流石に大変すぎる気がします。

ここで朗報なんですが、なんとAndroid SDKにはこれらをひとまとめにやってくれる汎用的なクラスが用意されています!AsyncTaskって言うんですけど、皆さんご存知でしょうか。

AsyncTaskの再発明にならないために

そんなわけで、今まで説明してきた内容を元に汎用的なクラスを作るとAsyncTaskを再発明してしまいます。これでは意味がありません。素直にそっちを使っておけと言う話になります。

自分でこの手の機構を作るメリットがあるのは以下のようなケースでしょう。

AsyncTaskがUIスレッド以外から呼べないのはちょっと複雑な事情があって、1つはonPreExecuteがexecuteを呼ばれたタイミングで実行されてしまうこと、もう1つはAsyncTaskが持っているHandler(の中で所持しているLooper)を初期化するタイミングによってはUIスレッド以外のLooperが渡されてしまうバージョンがある(※恐らくAPI Level 11あたりから対処されたのではないだろうか。少なくともver4.0.1以降は必ずUIスレッドのLooperで初期化されるよう工夫されている)ためだと思われます。

次回の内容

とまぁ、事前に説明しておかなければならない内容が多すぎてちょっと疲れてしまいました。

実際にどうやって実装するかだとかそう言った話はまた次回に。って言うか、作ったは作ったんですけど、まだ動かしてないので何とも言えないってのが実情ですが。

[2014/12/05追記] 次回と次々回へのリンクを貼ってなかった…。

参考