ちょっと話題の記事

[Android] 何人かの非同期処理を待ち合わせる | アドカレ2013 : SP #23

2013.12.23

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

Androidの非同期処理

モバイル・アプリケーションの開発において、非同期処理は必須です。
ユーザーのUI操作を妨げずに裏で様々な処理を行い、ユーザーが気持ちよく使えるUIでないと、マーケットで「ksアプリ」と言ったレビューや評価の星が1つという評価を受けてしまいます。

なるべく裏側でこっそり行いましょう

Androidでは非同期処理を実装する場合には、以下の様なクラスを使います

複数の非同期処理が完了したら、次の処理へ移行したい

それぞれの非同期処理を実行した時、個別個別で完結するならば問題ありません。
ただ、自分以外の処理がどうなっているかなどを知ることなど出来ないので、複数の非同期処理が完了したら次に行く、といった条件を付けたい場合困ります。
誰も音頭を取る人がいないので、待ち合わせる方法がありません。
これでは待ち合わせ時間のない修学旅行です。いつまで経っても修学旅行バスが発車できません。さて、困ったことになりました

要するにjQuery.deferred.promise のような機能がほしい

jQueryにはdeferredという素晴らしい機能が実装されています。Ajaxは非同期通信を基本としているので、この辺の同期機能は非常に手厚いですね

Javaには同期機構が存在します。Androidでも利用可能です。
java.util.concurrentパッケージ内にあるクラスを利用します。
今回はこのパッケージの中から、CountDownLatchCyclicBarrierを利用して、簡単に非同期処理の待ち合わせを行ってみます

CountDownLatch

ややこしいことが嫌いな人はこちらの方が良いかと思います。比較的直感的に分かりやすい動作の概念になっているので、よほどおかしなことをしない限りは、そこまでややこしくなることはないと思います

このクラスを利用すると、ON/OFFを切り替えるスイッチのようなものを提供してくれます。
非同期処理起動時に、呼び出し元からこのCountDownLatchのインスタンスが配布され、処理の完了後にこのLatchのスイッチをカウントダウンしてあげれば良いだけです。超ざっくりイメージは次の通り

countdownlatch_image

How to use

使い方はとても簡単。待ち合わせたい非同期処理の数を引数にインスタンスを生成します

// 3カウントのCountDownLatchを作成する
final CountDownLatch latch = new CountDownLatch(3);

各非同期処理にインスタンスをどうにかして渡します。

// CountDownLatchを受け渡し
asyncProcess1.setCountDownLatch(latch);

受け渡しが終わったら、待ちます。
ここはCountDownLatchを渡した非同期処理が全て完了するまで完全にブロックされるので、間違ってもUIスレッド内で実行しないようにしましょう

try {
	latch.await();
} catch (InterruptedException e) {
	e.printStackTrace();
}

非同期処理側では、処理が完了したら、渡されたCountDownLatchのオブジェクトのcountDown()を呼べばOKです

// 処理完了を通知
countDownLatch.countDown();

出力結果はこんな感じ。ID=3の最後の非同期処理が終わってから、All Thread is doneが表示されてる感じ

12-23 01:58:11.972: D/LoaderCallbackCDLImpl(16646): ID=5, Result=This Loader slept for 2000(ms)
12-23 01:58:14.965: D/LoaderCallbackCDLImpl(16646): ID=4, Result=This Loader slept for 5000(ms)
12-23 01:58:17.968: D/LoaderCallbackCDLImpl(16646): ID=3, Result=This Loader slept for 8000(ms)
12-23 01:58:17.988: D/CountDownLatchAsyncProcess(16646): All Thread is done

実装イメージとしてはこんな感じを目指します

implements_async

ちなみに

こちらのCountDownLatchは、AsyncTaskLoaderの中でも利用されています。AsyncTaskLoader内のCountDownLatchは1つだけ固定のようですが、うまくWaitをかけられるようになってるようです

ソースはこちら {SDK_HOME}/sources/android-18/android/content/AsyncTaskLoader.java

private final CountDownLatch mDone = new CountDownLatch(1);
/* Runs on the UI thread */
@Override
protected void onPostExecute(D data) {
    if (DEBUG) Slog.v(TAG, this + " onPostExecute");
    try {
        AsyncTaskLoader.this.dispatchOnLoadComplete(this, data);
    } finally {
        mDone.countDown();
    }
}

/* Runs on the UI thread */
@Override
protected void onCancelled(D data) {
    if (DEBUG) Slog.v(TAG, this + " onCancelled");
    try {
        AsyncTaskLoader.this.dispatchOnCancelled(this, data);
    } finally {
        mDone.countDown();
    }
}
/* Used for testing purposes to wait for the task to complete. */
public void waitForLoader() {
    try {
        mDone.await();
    } catch (InterruptedException e) {
        // Ignore
    }
}

CyclicBarrier

もう一つがCyclicBarrierです。
こちらはゴール直前で待っていて、全員揃ったらみんなで再スタートするようなイメージのようです。こんな感じ

cyclicbarrier_image

CountDownLatchと同じく複数の非同期処理の待ち合わせをするものです。色々とこちらは罠がありましたが、とりあえず

How to Use

使い方はこんな感じ。Barrierを作成します

/** 3つのThreadがバリアを通過したら、Logに出力 */
private CyclicBarrier barrier = new CyclicBarrier(3, new Runnable() {
	
	@Override
	public void run() {
		Log.d(TAG, "CyclicBarrierを全てのThreadが通過しました");
	}
});

作成したBarrierオブジェクトを各非同期処理で利用します。自分自身の処理が完了したら他が終わるまで待ちます。

private Runnable process = new Runnable() {
	
	@Override
	public void run() {
		int waitSeconds = generateAwaitSeconds() * 1000;
		Log.d(TAG, "waiting... for " + waitSeconds);
		try {
			Thread.sleep(waitSeconds);
			barrier.await();
		} catch (InterruptedException e) {
			e.printStackTrace();
		} catch (BrokenBarrierException e) {
			e.printStackTrace();
		}
		
		Log.d(TAG, "crossed barrier for " + waitSeconds);
	}
};

全て完了したら、再度動き出します。こんな出力結果に

12-23 01:46:18.366: D/StartCyclicBarrier(14370): waiting... for 2000
12-23 01:46:18.366: D/StartCyclicBarrier(14370): waiting... for 8000
12-23 01:46:18.366: D/StartCyclicBarrier(14370): waiting... for 2000
12-23 01:46:26.364: D/StartCyclicBarrier(14370): CyclicBarrierを全てのThreadが通過しました
12-23 01:46:26.364: D/StartCyclicBarrier(14370): crossed barrier for 8000
12-23 01:46:26.364: D/StartCyclicBarrier(14370): crossed barrier for 2000
12-23 01:46:26.364: D/StartCyclicBarrier(14370): crossed barrier for 2000

動作の違いについて

こちらは、CountDownLatchと動作が異なったため、自分は見事に罠を踏み抜きました。まずは動作の違いをまとめてみます

    

        

            

            

      

    

    

        

            

            

        

        

            

            

        

    

クラス名 特徴
CountDownLatch              非同期処理それぞれは、処理のFinishまで走る
             再利用不可能。都度作り直す
             処理を終了しきったら報告             
CyclicBarrier              非同期処理は、処理のFinish直前で停止
             再利用可能。reset()メソッドが用意されている
             指定された数の処理がFinish直前で停止したら再始動             

この動作の違いによって、CyclicBarrierはAsyncTaskLoaderでは利用できないようです。
というのもゴール直前で止めてしまうため、一つでもonFinish()に到達してしまうと他のLoaderの動作も含めて全てブロックしてしまう模様です。
CountDownLatchと同じようにAsyncTaskLoaderを利用したサンプルを作成しましたが、見事に応答が返ってこなくなりました。

NGだった例

NGだった例はCyclicBarrierとAsyncTaskLoaderを組み合わせた方法です

LoaderCallbacksの実装の一部。CyclicBarrierオブジェクトを渡してもらい、終了直前でWaitします

public void setCyclicBarrier(CyclicBarrier barrier) {
	this.cyclicBarrier = barrier;
}

@Override
public Loader<String> onCreateLoader(int id, Bundle args) {
	int awaitSeconds = args.getInt(AWAIT_SECONDS);
	return new DummyLoaderImpl(activity, awaitSeconds * 1000);
}

@Override
public void onLoadFinished(Loader<String> loader, String result) {
	try {
		// 全Threadが終わるまで待機
		Log.d(TAG, "ID=" + loader.getId() + " is waiting");
		cyclicBarrier.await();
		
		Log.d(TAG, "ID=" + loader.getId() + ", Result=" + result);
	} catch (InterruptedException e) {
		e.printStackTrace();
	} catch (BrokenBarrierException e) {
		e.printStackTrace();
	}
}

@Override
public void onLoaderReset(Loader<String> loader) {
}

AsyncTaskLoaderは指定時間Sleepするだけの簡単なものです

public class DummyLoaderImpl extends AsyncTaskLoader<String> {
	
	public static final String TAG = DummyLoaderImpl.class.getSimpleName();

	/** 待機時間 */
	private int awaitSeconds;
	
	public DummyLoaderImpl(Context context, int awaitSeconds) {
		super(context);
		this.awaitSeconds = awaitSeconds;
		Log.d(TAG, "Await Seconds = " + awaitSeconds);
	}
	
	@Override
	public String loadInBackground() {
		try {
			Thread.sleep(awaitSeconds);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		
		StringBuffer sb = new StringBuffer();
		sb.append("This Loader slept for ");
		sb.append(awaitSeconds);
		sb.append("(seconds)");
		return sb.toString();
	}
}

これを実行すると、実際は3本の非同期処理を走らせているにも関わらず、一本目がBarrierに到達した時点で、全ての動作が停止しました

AsyncTaskで試す

結果としてはこちらもNGでしたがAsyncTaskも試してみます。
AsyncTaskを継承します。同じくIntegerを引数にStringを返します。引数で指定した数値分Sleepさせます

public class CyclicAsyncTask extends AsyncTask<Integer, Void, String> {
	
	private CyclicBarrier barrier;
	
	public static final String TAG = CyclicAsyncTask.class.getSimpleName();
	
	private String taskName = "undefined";
	
	/**
	 * Constructor
	 * @param barrier
	 */
	public CyclicAsyncTask(CyclicBarrier barrier, String taskName) {
		this.barrier = barrier;
		this.taskName = taskName;
	}

	@Override
	protected String doInBackground(Integer... params) {
		int awaitSeconds = params[0];
		
		try {
			Thread.sleep(awaitSeconds * 1000);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		
		return "This Thread is slept for " + awaitSeconds + " (s)" ;
	}

	@Override
	protected void onPostExecute(String result) {
		super.onPostExecute(result);
		try {
			Log.d(TAG, taskName + " is waiting... : " + result);
			barrier.await();
		} catch (InterruptedException e) {
			e.printStackTrace();
		} catch (BrokenBarrierException e) {
			e.printStackTrace();
		}
		
		Log.d(TAG, taskName + " is crossed barrier");
	}
}

実行するコードは以下のとおり。
THREAD_POOL_EXECUTORの指定により並列実行されてるのは確認しています

CyclicAsyncTask task1 = new CyclicAsyncTask(barrier, "task1");
task1.executeOnExecutor(CyclicAsyncTask.THREAD_POOL_EXECUTOR, 10);

CyclicAsyncTask task2 = new CyclicAsyncTask(barrier, "task2");
task2.executeOnExecutor(CyclicAsyncTask.THREAD_POOL_EXECUTOR, 6);

CyclicAsyncTask task3 = new CyclicAsyncTask(barrier, "task3");
task3.executeOnExecutor(CyclicAsyncTask.THREAD_POOL_EXECUTOR, 3);

結果は以下のとおり

12-23 03:14:16.173: D/CyclicBarrierAsyncProcess(32238): All Thread is started
12-23 03:14:19.076: D/CyclicAsyncTask(32238): task3 is waiting... : This Thread is slept for 3 (s)

やはりいちばん早いAsyncTaskが到着した時点で動作が止まっていますね。
ちなみにこのawaitを削除すると以下のように動作しているので、やはり相性が悪いようです

12-23 03:20:21.730: D/CyclicBarrierAsyncProcess(1519): All Thread is started
12-23 03:20:24.673: D/CyclicAsyncTask(1519): task3 is waiting... : This Thread is slept for 3 (s)
12-23 03:20:24.673: D/CyclicAsyncTask(1519): task3 is crossed barrier
12-23 03:20:27.666: D/CyclicAsyncTask(1519): task2 is waiting... : This Thread is slept for 6 (s)
12-23 03:20:27.666: D/CyclicAsyncTask(1519): task2 is crossed barrier
12-23 03:20:31.659: D/CyclicAsyncTask(1519): task1 is waiting... : This Thread is slept for 10 (s)
12-23 03:20:31.659: D/CyclicAsyncTask(1519): task1 is crossed barrier

予測

予測するにこれは、先ほどCountDownLatchで提示した、AsyncTaskLoaderのCountDownLatchの動作が絡んでると思われます。
onLoaderFinish()で停止させてしまうと、AsyncTaskLoaderのonPostExecute()がいつまでも呼ばれません。

/* Runs on the UI thread */
@Override
protected void onPostExecute(D data) {
    if (DEBUG) Slog.v(TAG, this + " onPostExecute");
    try {
        AsyncTaskLoader.this.dispatchOnLoadComplete(this, data);
    } finally {
        mDone.countDown();
    }
}

/* Runs on the UI thread */
@Override
protected void onCancelled(D data) {
    if (DEBUG) Slog.v(TAG, this + " onCancelled");
    try {
        AsyncTaskLoader.this.dispatchOnCancelled(this, data);
    } finally {
        mDone.countDown();
    }
}

そのため、CountDownLatch#countDown()がいつまで経っても実行されないことが原因なのではないでしょうか。そのうち詳しく調べてみます。
今回はひとまずこんなところで。

まとめ

同期機構には、CyclicBarrierとCountDownLatchが使えます。
CountDownLatchは使い終えたら再利用はできないので、毎度作り直しが必要です。
CyclicBarrierは、AsyncTaskLoaderやAsyncTaskと相性が悪いものの、ある一点で非同期処理を全て完了するまで停止するということが可能です。

同期機構を使って非同期処理たちをうまく待ち合わせましょう

今回の悪あがきの結果はこちら
Github - BlogSample

参考