LoginSignup
2
1

More than 3 years have passed since last update.

Kotlin 1.3のCoroutineのサスペンド関数の合成④(asyncな関数)

Posted at

検証環境

この記事の内容は、以下の環境で検証しました。

  • Intellij IDEA ULTIMATE 2018.2
  • Kotlin 1.3.0
  • Gradle Projectで作成
  • GradleファイルはKotlinで記述(KotlinでDSL)

準備

詳細は下記の準備を参照してください。
https://qiita.com/naoi/items/8abf2cddfc2cb3802daa

Async-style functions

前回に引き続き、公式サイトを読み解いていきます。

タイトルと読むと漫画の題名みたいです。
タイトルだけではよくわからないので、読んでみましょう。

We can define async-style functions that invoke doSomethingUsefulOne and doSomethingUsefulTwo asynchronously using async coroutine builder with an explicit GlobalScope reference. We name such functions with "Async" suffix to highlight the fact that they only start asynchronous computation and one needs to use the resulting deferred value to get the result.

訳してみると。

doSomethingUsefulOneやdoSomethingUsefulTwo関数を呼び出すasync-style関数を定義できます。その関数はasyncコルーチンビルダーを使用することにより、明示的にGlobalScopeのコルーチンを定義できます。
その様に定義した関数の接尾語として、 Async を付与します。接尾語を付与することにより、非同期処理を実装し、処理完了後に結果をかえすことを表せます。

なるほど、関数そのものがasync処理だけを実装しているような感じでしょうか。
サンプルコードもあるので確認してみます。

// The result type of somethingUsefulOneAsync is Deferred<Int>
fun somethingUsefulOneAsync() = GlobalScope.async {
    doSomethingUsefulOne()
}

// The result type of somethingUsefulTwoAsync is Deferred<Int>
fun somethingUsefulTwoAsync() = GlobalScope.async {
    doSomethingUsefulTwo()
}

GlobalScope.asyncでasync-style関数になるんですね。接尾語にその旨を表す命名になっています。

続けて読み進めてみます。

Note, that these xxxAsync functions are not suspending functions. They can be used from anywhere. However, their use always implies asynchronous (here meaning concurrent) execution of their action with the invoking code.

The following example shows their use outside of coroutine:

訳してみると

このasync-style関数にはsuspendが関数に付与されていないことに注意して下さい。
これらの関数は様々な場所から呼び出せます。しかし、関数を呼び出すコードとは非同期(平行)で実行されます。

これらの関数を呼び出すサンプルコードを確認してみましょう。

確かに、関数にsuspendが付与されていないですね。関数の処理内でコルーチンビルダーが働くので、呼び出し元のスレッドとは別として動きそうです。
サンプルコードをみています。

import kotlinx.coroutines.*
import kotlin.system.*

// note, that we don't have `runBlocking` to the right of `main` in this example
fun main() {
    val time = measureTimeMillis {
        // we can initiate async actions outside of a coroutine
        val one = somethingUsefulOneAsync()
        val two = somethingUsefulTwoAsync()
        // but waiting for a result must involve either suspending or blocking.
        // here we use `runBlocking { ... }` to block the main thread while waiting for the result
        runBlocking {
            println("The answer is ${one.await() + two.await()}")
        }
    }
    println("Completed in $time ms")
}

fun somethingUsefulOneAsync() = GlobalScope.async {
    doSomethingUsefulOne()
}

fun somethingUsefulTwoAsync() = GlobalScope.async {
    doSomethingUsefulTwo()
}

suspend fun doSomethingUsefulOne(): Int {
    delay(1000L) // pretend we are doing something useful here
    return 13
}

suspend fun doSomethingUsefulTwo(): Int {
    delay(1000L) // pretend we are doing something useful here, too
    return 29
}

somethingUsefulTwoAsync関数などにsuspendは付与されていないけど、その関数内で呼び出しているdoSomethingUsefulOne関数はさすがにsuspendが付与されていますね。

実行してみるとこのような結果になります。

The answer is 42
Completed in 1176 ms

確かに並列処理しているから1秒程度であることがわかります。

そのあと、説明が続いているので読み進めてみます。

This programming style with async functions is provided here only for illustration, because it is a popular style in other programming languages. Using this style with Kotlin coroutines is strongly discouraged for the reasons that are explained below.

訳してみます。

このような非同期処理のコーディングスタイルは一般的であるため、説明用としてサンプルコードを提供しています。Kotlinでは、以下の理由から進めていません。

えっ!サンプルコードを出しておいて推奨しないって・・・・。
理由が気になるので、読み進めてみます。

Consider what happens if between val one = somethingUsefulOneAsync() line and one.await() expression there is some logic error in the code and the program throws an exception and the operation that was being performed by the program aborts. Normally, a global error-handler could catch this exception, log and report the error for developers, but the program could otherwise continue doing other operations. But here we have somethingUsefulOneAsync still running in background, despite the fact, that operation that had initiated it aborts. This problem does not happen with structured concurrency, as shown in the section below.

訳します。

val one = somethingUsefulOneAsync() とone.await()の間でエラーが発生した場合を考えてみましょう。サスペンド関数が完了するその間にエラーが発生した場合、もちろんExceptionが発生して、somethingUsefulOneAsync関数の処理は中断します。通常、グローバルエラーハンドルはこの例外をキャッチして、ログやレポートを出力します。しかし、他のコルーチンは動作しつづけます。
今回の例だと、somethingUsefulOneAsync関数が動き続けます。このような現象は、次のセクション(記事)のように記述すれば問題ありません。

たしかにそれはあり得る話ですね。
しかも、ここまで来て次の記事を読むように指定されています。
せっかくなので、試してみようと思います。

doSomethingUsefulOne関数内で例外を発生させるように修正してみます。
以下のように修正してみました。

package step04

import kotlinx.coroutines.*
import java.io.IOException
import kotlin.system.*

// note, that we don't have `runBlocking` to the right of `main` in this example
fun main() {
    val time = measureTimeMillis {
        // we can initiate async actions outside of a coroutine
        val one = somethingUsefulOneAsync()
        val two = somethingUsefulTwoAsync()
        // but waiting for a result must involve either suspending or blocking.
        // here we use `runBlocking { ... }` to block the main thread while waiting for the result
        runBlocking {


            try {
                println("The answer is ${one.await() + two.await()}")
            } catch (e: Exception) {
                println("エラーが発生しました")
                delay(1000)
            }
        }
    }
    println("Completed in $time ms")
}

fun somethingUsefulOneAsync() = GlobalScope.async {
    doSomethingUsefulOne()
}

fun somethingUsefulTwoAsync() = GlobalScope.async {
    doSomethingUsefulTwo()
}

suspend fun doSomethingUsefulOne(): Int {
    delay(500L) // pretend we are doing something useful here
    throw IOException()
}

suspend fun doSomethingUsefulTwo(): Int {

    repeat(10){
        println("${it} times sleep")
        delay(100) // pretend we are doing something useful here, too
    }
    return 29
}

エラーを発生させたうえで、try-catchをしてdelay関数を呼び出しています。doSomethingUsefulTwo関数では0.1秒毎に標準出力でメッセージを出力しています。

結果はこの様になりました。

0 times sleep
1 times sleep
2 times sleep
3 times sleep
4 times sleep
エラーが発生しました
5 times sleep
6 times sleep
7 times sleep
8 times sleep
9 times sleep
Completed in 1607 ms

この結果から読み取れることは、doSomethingUsefulOne関数がエラーで終了しても別スレッドで実行しているdoSomethingUsefulTwo関数は動作し続けることです。

説明文に書いてあるとおりの動作をしています。

まとめ

このブロックで理解できたことは以下のことだと思います。

  • 複数のコルーチンを呼び出すとそれぞれ別スレッドで動作するため、気づかずに他のコルーチンが動作し続ける状態で次の処理に遷ると思わぬ不具合を起こすこと
2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1