サイトロゴ

Enjoy Creating
Web & Mobile Apps

MENU BOX
WEB
MOBILE
OPEN

ホーム

 > 

 > 

【Kotlin】非同期処理(Coroutine)の基本のキ

【Kotlin】非同期処理(Coroutine)の基本のキ

この記事にはプロモーションが含まれています。

非同期処理は様々なアプリケーションで活用されている技術で、その理解はプログラミング学習においても一つの重要な関門です。

Kotlinにおける非同期処理は、Coroutine(単数形)またはCoroutines(複数形)と呼ばれています。(※以下、コルーチンと表記)

Kotlinのコルーチンはシンプルに記述できるように設計されている他、エラーが発生した際のエラーハンドリングが直感的で、非同期処理の中でもコードの可読性を保ちやすいという特徴があります。

非同期処理とそれに関する機能は、Kotlinに限らず多くのプログラミング言語がサポートしていますが、その扱いやすさという点で、『Kotlinの特徴 = コルーチン』というふうに表現されることも多いです。

この記事では、Kotlinのコルーチンの基本中の基本を理解することを目標に、サンプルコードを提示しながら解説を進めていきます。

なお、この記事ではKotlinのコルーチンの基礎部分のみに範囲を絞っており、応用的な内容までカバーしていない点は、予めご了承ください!

この記事を読むことで分かること
  • ・非同期処理とは?
  • ・コルーチンの基本
  • ・コルーチンの作成とキャンセル

– 目次 –

そもそも非同期処理って?なぜ必要?

Kotlinにおけるコルーチンの説明に入る前に、まず、そもそもアプリケーションにはなぜ非同期処理という仕組みが必要なのかを確認しておきましょう。

プログラムは通常、記述した順(上から下)に実行されます。

たとえば、次のような一連の処理を例にしてみましょう。

  1. ファイルのダウンロード開始を知らせるメッセージを表示
  2. ファイルのダウンロード処理
  3. ダウンロードが完了したかどうかを知らせるメッセージを表示

図で表すと下のようなイメージです。

同期的に処理が行われるイメージ図

一見、特に問題がないように思えますが、ここに『ダウンロードを途中でキャンセルできるようにする機能』を加えたくなったらどうしましょう?

この時、キャンセルするためのプログラムを下のように(2)(ダウンロード処理)の下に記述しても意味がありません。

  1. ファイルのダウンロード開始を知らせるメッセージを表示
  2. ファイルのダウンロード処理
  3. ダウンロードをキャンセルする処理
  4. ダウンロードが完了したかどうかを知らせるメッセージを表示

なぜならば、プログラムは記述順に実行されるので、(2)のダウンロード処理が終わった後でなければその次に記述されたキャンセル処理は実行されないからです。

ダウンロードが完了してからキャンセル処理が始まっても、『時すでに遅し』ですよね。笑

こんな時に活躍するのが、非同期処理と呼ばれる仕組みです。

ファイルのダウンロード処理の完了を待つのではなく、状況に応じて中断できるようにしておき、ダウンロード処理の途中でも他の処理を実行可能な状況にすることができれば、キャンセル処理を導入できます。

この仕組み(流れ)が非同期処理であり、処理の流れは次のように表すことができます。

  1. ファイルのダウンロード開始を知らせるメッセージを表示
  2. ファイルのダウンロードに関連する処理
    1. ファイルのダウンロード処理(非同期処理=完了を待たない)
    2. ダウンロードのキャンセル処理
  3. ダウンロードが完了したかどうかを知らせるメッセージを表示
非同期的に処理が行われるイメージ図

ざっくりではありますが、以上が非同期処理の流れと、アプリケーションにおいて非同期処理が必要となる理由です。

ここではファイルのダウンロードとキャンセルを非同期処理の典型例として取りあげましたが、他にも様々なシチュエーションで非同期処理が行われています。

もし非同期処理が行われなければ、ユーザーがアプリで何かアクションを起こすたびにアプリが処理の完了を待つこととなり、アプリが固まった(停止した)かのような挙動をユーザーに見せてしまいます。

しょっちゅう固まるアプリなんて使い勝手が悪いですし、ユーザー体験を悪くしてしまいますよね。

快適なユーザー体験を提供するために、多くのアプリケーションでは非同期処理が頻繁に行われており、必要不可欠な存在となっています。

    POINT!
  1. ・同期的処理はコードの記述順に処理が行われる(直前の処理の完了を待つ)!
  2. ・非同期処理では、処理の完了を待たずに別の処理を行うことができる!

Kotlinにおける非同期処理関数の宣言

Kotlinにおいては、非同期的に処理を行わせたい関数やメソッドに suspend キーワードをつけることで、その処理が非同期的に行われるようになります。

ちなみに suspend には『一時停止する・一時中断する』といった意味があり、まさに、『中断可能な(完了を待たない)処理である』ということを意味します。

ただし、関数を suspend fun として宣言するだけで非同期処理が行えるわけではありません。

Kotlinでは、非同期処理(suspend関数)はコルーチンスコープと呼ばれる非同期処理用のスコープ内(もしくは他のsuspend関数)でしか呼び出すことができないようになっています。

なので、たとえば下のコードはエラーになってしまいます。

Kotlin

suspend fun doSomething() {
    // do something as asynchronous processing
    println("Process has done!")
}

fun main() {
    doSomething() // This causes an error because it isn't called in a coroutine scope
}

/* Error Message:
 * Suspend function 'doSomething' should be called only from a coroutine or another suspend function
 */

コルーチンスコープには色々種類があり、ここではその全てについて触れることはできませんが、テスト用としては、よく runBlocking が使用されます。

コードを次のように修正すれば、エラーを起こさず処理を実行できます。

Kotlin

import kotlinx.coroutines.*

suspend fun doSomething() {
    // do something as asynchronous processing
    println("Process has done!")
}

fun main() {
    runBlocking { // Starting the coroutine scope
        doSomething()
    }
}

一行目にimport文が挿入されていますが、一般的に、Kotlinにおけるコルーチンの基本機能は kotlinx.coroutines ライブラリに含まれています。

なので、Kotlinのコルーチンについて学習する際には、基本的にライブラリ(kotlinx.coroutines)を読み込む必要があると認識しておくと良いかと思います。

また、runBlockingはコルーチンスコープを作り出すのに便利で、前述の通りテストやサンプルコードではよく使用されますが、スコープ内のコルーチンが完了するまでスレッドをブロックしてしまうため、プロダクション環境のアプリケーションでの使用は推奨されません。

プログラミングの学習を始めたばかりの場合は、まだスレッドやブロックという言葉に馴染みがないかと思いますが、とにかく『runBlockingはテストやmain関数での使用に限られる』という認識を持っておくと良いでしょう。

何はともあれ、このステップではコルーチンスコープ内で非同期処理関数(suspend関数)を呼び出すことに成功しました。

しかし、現段階ではプログラムを実行しても『Process has done!』という文字列だけが出力されるだけなので、本当に非同期的に処理が行われているのかどうかが確認できませんね。

次のステップでは、さらにコードに手を加えて、実際に非同期的に処理が行われることをわかりやすくしてみたいと思います。

    POINT!
  1. ・非同期的に処理を行いたい場合は、suspendキーワードをつけて関数を宣言する!
  2. ・suspend関数はコルーチンスコープや他のsuspend関数でしか呼び出すことができない!

非同期処理の実行と確認

このステップでは、suspend関数である doSomething() の処理に時間がかかるようにすることで、『コードでは doSomething() の方が先に記述・実行しているのに、後に記述した他の処理の方が先に完了する』ことを確認してみましょう。

delay() を使って、『”Process has done!”』のメッセージの表示(println)の実行まで2秒ほどかかるようにします。

Kotlin

suspend fun doSomething() {
    // do something as asynchronous processing
    delay(2_000L) // add this code
    println("Process has done!")
}

そして、doSomething関数の実行後に、『Hello!』というメッセージを表示する処理を用意します。

Kotlin

doSomething()
println("Hello!") // add this code

しかし、このままでは、結局『Process has done!』のメッセージの後に『Hello!』が表示されることとなり、非同期処理が行われていることが確認できません。

Kotlin

import kotlinx.coroutines.*

suspend fun doSomething() {
    // do something as asynchronous processing
    delay(2_000L)
    println("Process has done!")
}

fun main() {
    runBlocking { // Starting the coroutine scope
        doSomething()
        println("Hello!")
    }
}

/* Out puts:
 * Process has done!
 * Hello!
 * */

これは、同一のコルーチン内に2つの処理を記述しているからです。

コルーチンスコープ内では非同期的に処理が行われますが、同一のコルーチン内ではシーケンシャルに(順序通りに)実行されます。(※これは他の非同期操作をブロックするものではありません)

suspend関数である doSomething() の処理の完了を待たないようにするには、doSomething()println("Hello!") をそれぞれ別のコルーチンで実行する必要があります。

そこで出てくるのが launch です。launch はスレッドをブロックせずに新しいコルーチンを生成します。

launch を使ってコードを次のように修正すると、println("Hello!")doSomething() の完了を待たなくなり、先に『Hello!』の文字列が表示されることとなります。

Kotlin

import kotlinx.coroutines.*

suspend fun doSomething() {
    // do something as asynchronous processing
    delay(2_000L)
    println("Process has done!")
}

fun main() {
    runBlocking { // Starting the coroutine scope
        launch { // Building the new coroutine
            doSomething()
        }
        println("Hello!")
    }
}

/* Outputs:
 * Hello!
 * Process has done!
 * */

doSomething() よりも後に記述・実行しているはずの println("Hello!") による出力の方が先になりましたね!

この出力結果から、非同期的に処理が行われていることが確認できました。

Kotlinにおける非同期処理の基本中の基本に関する説明は以上ですが、せっかくなので冒頭で例にした『ファイルのダウンロード処理を非同期的に行い、それをキャンセルする』という流れもKotlinで表現してみたいと思います。

もちろん、実際にダウンロードを行うコードを記述するわけではなく、あくまで擬似的なものになりますが、このステップで紹介したサンプルコードと比べると少しだけ実践的なものになります。

    POINT!
  1. ・delay()で指定した時間だけ処理を待たせることができる(テスト時に特に有用)!
  2. ・launchは新しいコルーチンを生成する!

(おまけ)非同期処理をキャンセルする

非同期処理では、処理の完了を待たないだけでなく、実行中の処理をキャンセルすることも可能です。

具体的には、launch によって生成されたコルーチンから返されるジョブに対して、cancelcancelAndJoin のようなメソッドを使用してキャンセル命令を出すことができます。

cancel は単にコルーチンをキャンセルしますが、cancelAndJoin はキャンセルの後、その終了を待ちます。これにより、コルーチンが安全に停止することを保証できます。

より詳細な情報については、Kotlinの公式ドキュメントを参照してください。
リンク:kotlinx.coroutines

以上を踏まえて、ファイルのダウンロード処理をキャンセルするコードを擬似的に表現してみましょう。

まず、ダウンロードが完了したかどうかを示す変数を用意しておきます。初期値は当然 false です。

Kotlin

var isDownloaded = false // Initially assuming the download is not completed (false)

そして、ファイルのダウンロードを実行する関数を宣言します。これは非同期的に行われる必要があるので、suspend関数として宣言します。

Kotlin

suspend fun downloadFile(): Boolean {
    try {
        // Simulating a download process with a delay
        delay(5_000L)
        return true  // Assuming the download is successful
    } catch (e: CancellationException) {
        // Handling the case where the process is cancelled
        println("The download has been cancelled by the user.")
        throw e  // Rethrowing the cancellation event
    }
}

この関数では、ダウンロードが完了したら true を返すようにして、キャンセルされたらそれを示すメッセージとキャンセルイベントをスローするようにしています。

あとはダウンロードが始まった後で、すこし時間が経ってから(ダウンロードが完了する前に)キャンセル処理を行います。

Kotlin

println("Starting the download.")

val job = launch {
    isDownloaded = downloadFile() // Executing the download
}

// Waiting a bit before cancelling the job
delay(1_000L)  // Waiting for the download to start
job.cancelAndJoin() // Cancelling the job and waiting for its completion (assuming it's cancelled by the user)

これで、downloadFile() は実行中の状態ですが完了を待たずにジョブがキャンセルされ、isDownloadedfalse のままになります。

コードの全体像は次のようになります

Kotlin

import kotlinx.coroutines.*

var isDownloaded = false // Initially assuming the download is not completed (false)

suspend fun downloadFile(): Boolean {
    try {
        // Simulating a download process with a delay
        delay(5_000L)
        return true  // Assuming the download is successful
    } catch (e: CancellationException) {
        // Handling the case where the process is cancelled
        println("The download has been cancelled by the user.")
        throw e  // Rethrowing the cancellation event
    }
}

fun main() {
    runBlocking { // Starting the coroutine scope
    
        println("Starting the download.")

        val job = launch {
            isDownloaded = downloadFile() // Executing the download
        }

        // Waiting a bit before cancelling the job
        delay(1_000L)  // Waiting for the download to start
        job.cancelAndJoin() // Cancelling the job and waiting for its completion (assuming it's cancelled by the user)
    }
    
    // Displaying the final message about the download status
    if(isDownloaded) {
        println("The download has completed successfully.")
    } else {
        println("The download has not completed.")
    }
    
    println("Download status: $isDownloaded")
}

このコードを実行すると、まず、println によって “Starting the download.” の文字列が出力されます。

その後、launch によって生成されたコルーチン内で非同期的にsuspend関数として定義されている downloadFile() が実行され、ここでは返されるジョブを job として受け取っています。

次に、delay(1_000L) によって、downloadFile() の処理が進むのを1秒間待っています。

そして、job.cancelAndJoin() によって非同期的に行われているジョブをキャンセルしています。

downloadFile() の完了には少なくとも5秒以上かかる(※)ので、擬似的なダウンロードが完了するよりも早く、キャンセル命令が出されることとなります。
※delay(5_000L)が含まれるため

結果的に、擬似的なダウンロード処理はキャンセルされ、ダウンロード処理は完了していないことを示すメッセージが最後に表示されることとなります。

また、繰り返しになりますが、このコードは実際にファイルのダウンロードを行うものではなく、あくまで一連の流れを擬似的に表現したものであることに注意してください。

このように、非同期処理が必要となる状況を擬似的に表現することは、Kotlinのコルーチンを理解するのに大いに役立ちます。

余裕があればぜひ、ファイルのダウンロードだけでなく他の状況(例:天気予報のAPIを利用して天気予報データを取得するなど)も想定して、Kotlinのコルーチンを利用して擬似的な状況再現コードを書いてみてくださいね!

    POINT!
  1. ・非同期処理では処理をキャンセルすることもできる!
  2. ・cancelAndJoin()はジョブをキャンセルし、キャンセルの終了を待つ!

« »

カテゴリーリンク

著者について- author profile -

ROYDOプロフィール写真
Michihiro

モバイルアプリ(iOS・Android)ディベロッパー&デザイナー

これまでに、可読性の高いカラーパターンを自動で生成するアプリや、『第3火曜日』といった形式で通知をスケジュールできるアプリなどを制作。

サブでWebデザイン・フロントエンドエンジニアとしても活動しています。

📝ツール・言語:JavaScript/React Native/Kotlin/Android Studio/Swift/SwiftUI

🎓資格:基本情報技術者/ウェブデザイン技能検定3級

Twitterアイコン Instagramアイコン