何十年もの間、「重い処理を行なって画面が固まる事象にどう対応するかという問題に人々は立ち向かってきた。
このページには先人が今まで考えてきたアプローチが簡潔かつわかりやすくまとまっているので、一つずつ見ていく。
Threading
いわゆる「スレッド」。
非同期処理の中では最もポピュラーなものの一つ。
fun postItem(item: Item) {
val token = preparePost()
val post = submitPost(token, item)
processPost(post)
}
fun preparePost(): Token {
// makes a request and consequently blocks the main thread
return token
}
もしpreparePost関数が長時間実行される場合、その処理は別のスレッドに逃すことでUIのブロックを回避できる。
一方、この方法にはいくつか欠点もある。
-
スレッドは安価ではない。スレッドにはコンテキストスイッチが必要だが、それはコストがかかる
※ コンテキストスイッチ:CPUが処理を中断してメモリに保存し、それを取り出して別の処理を再開すること -
スレッドは無限ではない。起動できるスレッドの数にはシステムによって制限される
-
スレッドはフレームワークによってはサポートしていない可能性がある
-
スレッドは扱いが難しく、デバッグや競合の回避に気を配る必要がある
コールバック
コールバックはある関数を別の関数の引数として渡し、重い処理が完了した後に引数の関数を実行するという考え方。
fun postItem(item: Item) {
preparePostAsync { token ->
submitPostAsync(token, item) { post ->
processPost(post)
}
}
}
fun preparePostAsync(callback: (Token) -> Unit) {
// make request and return immediately
// arrange callback to be invoked later
}
上記の場合、preparePostAsyncが重い処理だったとしても、処理が完了したのを待ってから引数のsubmitPistAsyncが実行される形となる。
これは洗練された解決策に思えるが、以下のような欠点がある
- ネストされたコールバックは可読性が落ちる
- エラー処理が複雑になる
なお、こうしたコールバックはJavaScriptのようなイベントループアーキテクチャでは一般的である。
Futures, promises, and others
fun postItem(item: Item) {
preparePostAsync()
.thenCompose { token ->
submitPostAsync(token, item)
}
.thenAccept { post ->
processPost(post)
}
}
fun preparePostAsync(): Promise<Token> {
// makes request and returns a promise that is completed later
return promise
}
これは呼び出しを行うとPromise型(あらかじめ定められた型)を返す保証があり、それに対して捜査を行っていくというもの。
上記の場合、preparePostAsyncはPromise型を返すことになっている。
この方法はプログラミング方法に以下のような変更が必要になる。
- このプログラミングモデルはトップダウン型の命令型からコールバックチェーンのような構成型モデルへ移行する。ループや例外処理などの従来のプログラム構造が使えないことになる
- thenComposeやthenAcceptなどの新しいAPIを学習する必要がある
- 戻り値の型が実際に必要な型から変わっている
- エラー処理が複雑になる
Reactive extensions
いわゆるRxというもの。
Rxの背後に流れる思想として、「observable streams」というものがある。
これはデータを無限量のストリームであり、これらのストリームは観測可能であるという考え方である。
アプローチはFutureと似ているが、Rxはストリームを返すという点で異なる。
このことを表す表現として、「"everything is a stream, and it's observable"」という表現がある。
コルーチン
コルーチンは実行を中断するという考え方、つまり関数が途中で実行を中断し、後で再開するという考え方に基づいている。
一方で、開発者はこのことをほぼ意識せず、通常のnon-blockingなコードと同様に記述することができる。
fun postItem(item: Item) {
launch {
val token = preparePost()
val post = submitPost(token, item)
processPost(post)
}
}
suspend fun preparePost(): Token {
// makes a request and suspends the coroutine
return suspendCoroutine { /* ... */ }
}
例えば上記のコードの場合、postItem関数はlaunchスコープがついていることを除いてはほぼ通常関数と同様に記述ができる。
preparePost関数は時間のかかる処理であるが、この関数にはsuspendがついており、中断・再開可能であることを示している。つまり時間がかかる処理があってもスレッドを占有せず、待ち時間があれば処理を中断して別の処理にスレッドを開け渡すことができるのである。