3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Codelab コルーチンの概要(Introduction to coroutines)

3
Last updated at Posted at 2023-09-08

はじめに

「コルーチンってなに?」という状態であったので、コルーチンの概要というAndroidアプリ開発におけるコルーチンの基礎を説明してそうなCodelabを進めて理解を深めます。

本文を記入しつつ、つまづいた部分や気になった部分は本文の間に挟む形で、メモ書き程度にここに記入していきます。また、説明は自体は4章までなので5章以降は割愛します。

この記事がコルーチンの理解の手助けになれば幸いです。

1.準備

レスポンシブ UI は、優れたアプリに不可欠な要素です。これまでに作成したアプリでは当たり前のことだったかもしれませんが、ネットワーキングやデータベース機能といった高度な機能を追加し始めると、機能性とパフォーマンスの両方を備えたコードを記述することがますます難しくなります。次の例は、インターネットから画像をダウンロードするような長時間実行タスクが正しく処理されなかった場合にどうなるかを示しています。画像機能は動作していますが、スクロールが飛び飛びになるため、UI が応答しないように見えます(ふさわしくありません)。
9f8c54ba29f548cd.gif
上述のアプリの問題を回避するには、スレッドという処理について少し学ぶ必要があります。スレッドは少し抽象的な概念ですが、アプリのコードに対する単一の実行パスと考えることができます。記述するコードの各行は、同じスレッドで順番に実行される命令です。

<メモ開始>
出ました!スレッド!なんとなく非同期処理で使うやつ?(非同期処理も名前しか聞いたことない)みたいなイメージがありますが、何なのかよく分かってないやつですね!
検索すると私がいつもお世話になっている
「分かりそう」で「分からない」でも「分かった」気になれるIT用語辞典がヒットしたので覗いてみます。スレッドの説明ページを読むと、2種類の手法でカレーを作るプログラムのフローチャートを使ってスレッドを説明しています。

プログラム1 ご飯を炊く → カレーを煮込むを順番に行う手法

プログラム2 ご飯を炊くとカレーを煮込むを同時並行で行う手法

これらを踏まえ、下記がスレッドであると述べられています。

このとき動いている、それぞれの処理(の単位)が「スレッド」です。

イメージしづらければ、処理開始から処理終了までの流れを線で引いて、その線一本一本がスレッドだと思っても構いません。

そのため、線が1本のプログラム1はスレッドが1つ、線が2本のプログラム2はスレッドが2つとなるそうです。なんだかざっくりスレッドが何かは理解できたかもしれませんね!

<メモ終了>

すでに Android でスレッドを扱っています。すべての Android アプリには、デフォルトの「メイン」スレッドがあります。これは(通常は)UI スレッドです。これまでに記述したコードはすべて、メインスレッド上にあります。各命令(コード行)は、前の命令が終了するまで待ってから、次の行を実行します。

<メモ開始>
メインスレッドはなんか聞いたことありますね。よくAndroid公式ドキュメントに出てくるイメージ。調べると、公式ドキュメントに以下のようなメインスレッドの説明がありました。

アプリの起動時に、システムはアプリ実行用のスレッドを作成します。これは、「メインスレッド」と呼ばれます。このスレッドは、イベント(描画イベントを含む)を適切なユーザー インターフェース ウィジェットに送信する役割を担うため非常に重要です。

UI スレッドまたは「メイン」スレッド以外のスレッドから UI を更新することはできません。

そのため、UIに関することを行うスレッドはメインスレッドであると言えそう??
<メモ終了>

ただし実行中のアプリには、メインスレッドに加えてさらに多くのスレッドがあります。背後で、プロセッサは実際に別々のスレッドで動作するのではなく、異なる命令の間を行き来してマルチタスクのように見せかけます。スレッドは抽象化であり、コードを記述するとき、各命令を実行する実行パスを決定するために使用できます。メインスレッド以外のスレッドを操作すると、アプリのユーザー インターフェースが応答したまま、バックグラウンドで画像のダウンロードなどの複雑なタスクを実行できます。これを「同時実行コード」、または単に「同時実行」といいます。

<メモ開始>

メインスレッド以外のスレッドを操作すると、アプリのユーザー インターフェースが応答したまま、バックグラウンドで画像のダウンロードなどの複雑なタスクを実行できます。これを「同時実行コード」、または単に「同時実行」といいます。

同時実行というと、先ほどの2つのスレッドを持っていたカレー作成のプログラムを思い出しますね。ご飯を炊くこととカレーを煮込むことをそれぞれ別のスレッドで同時に実行していたので、その状況と同じことを言っているのでしょうか。
<メモ終了>

この Codelab では、スレッドの詳細について学びます。また、コルーチンと呼ばれる Kotlin 機能を使用して明確なノンブロッキング同時実行コードを記述する方法について学びます。

<メモ開始>
また聞きなれない言葉が出てきました。ノンブロッキングってなんだ?
またもやピヨ太先生に教えを乞います。ノンブロッキングのページを読んでみると、

ノンブロッキング(英:non-blocking)とは
「○○している最中も処理は進むけど、○○が終わらないとできない処理になったら待ってるよ~」のことです。

と書かれています。これだけじゃよくわからん。
読み進めると、

  1. 選択
  2. 掃除
  3. アイロンがけ

の3つの家事を行う様子で、ノンブロッキングとその反対のブロッキングを説明しています。

まずはノンブロッキングから

まずは洗濯からです。
ピヨ太君は洗濯機に服とかを放り込んで開始ボタンをポチッと押しました。

次は掃除です。
洗濯機が動き出したら、ピヨ太君は部屋の掃除を始めました。

最後はアイロンがけです。
掃除が終わったらアイロンがけ……と思ったのですが、まだ洗濯機は動いています。

アイロンがけは洗濯が終わった後でないとできませんよね。
ピヨ太君は洗濯が終わるのを、ぼーっとしながら待ちました。

洗濯が終わったら、アイロンがけを始めます。

この話におけるピヨ太君のような動きがノンブロッキングです。

1.洗濯している最中も処理(掃除)は進む
2.洗濯が終わらないとできない処理(アイロンがけ)になったら中断して待つ

ノンブロッキングでは処理が枝分かれするイメージです。

ノンブロッキングの説明はここまでです。最後の画像をみると、スレッドの説明ページの2つのスレッドがある処理(ご飯炊きとカレー煮込みを同時並行で行う方の処理)と図が似てますね。今回の例も複数スレッドでの処理と表現したりするのでしょうか。なんだかノンブロッキングは複数スレッドの処理と密接に絡んできそうな感じがしますね。

続いてはブロッキングの説明です。

まずは洗濯からです。
ピヨ太君は洗濯機に服とかを放り込んで開始ボタンをポチッと押しました。

ピヨ太君は、洗濯が終わるのを、ぼーっとしながら待ちます。

洗濯が終わりました。
次は掃除です。
ピヨ太君は部屋の掃除を始めました。

掃除が終わりました。
最後はアイロンがけです。
ピヨ太君はアイロンがけを始めました。

この話におけるピヨ太君のような動きがブロッキングです。
ブロッキングでは処理が一本道で進みます。

ブロッキングは、やることを1つずつ順番に終わらせていきます。

ブロッキングの説明はここまでです。
ここでも矢印を用いた図が登場しました。スレッドの説明ページの1つのスレッドの処理(ご飯炊きとカレー煮込みを順番に行う方の処理)と似ていますね。ブロッキングは1つのスレッドの処理であると言い換えることができるのでしょうか。

長々と説明がありましたが、つまり、

  • 同時実行できる場合は同時並行的に処理を行い、同時実行が不可能な場合は処理を順番に行うのがノンブロッキング
  • 同時実行せず全ての処理を順番に行うのがブロッキング

だと解釈できそう?
<メモ終了>

2.概要

マルチスレッドと同時実行

これまで、単一の実行パスを持つプログラムとして Android アプリを扱ってきました。単一の実行パスで多くのことができますが、アプリの規模が大きくなるにつれ、同時実行について考える必要が出てきます。

同時実行を使用すると、複数のコード単位を順不同で実行したり、並列かのように実行したりできるため、リソースをより効率的に使用できます。オペレーティング システムは、システム、プログラミング言語、同時実行単位の特性を使用して、マルチタスクを管理できます。

image.png

<メモ開始>
この画像でいうと、

  • Single Path of Execution : ブロッキングな処理(同時実行せず全ての処理を順番に行う)
  • Concurrency : ノンブロッキングな処理(同時実行できる場合は同時並行的に処理を行う)

ってことなんだろうか?
<メモ終了>

同時実行を使用する必要があるのはなぜでしょうか?アプリの複雑性が増すと、コードがノンブロッキングであることが重要になります。つまり、ネットワーク リクエストなどの長時間実行タスクを実行しても、アプリ内の他のタスクが実行されなくなることはありません。同時実行が適切に実装されていないと、アプリが応答していないように見えることがあります。

Kotlin での同時実行プログラミングを紹介する例をいくつか見てみましょう。サンプルはすべて、Kotlin プレイグラウンドで実行できます。

Kotlin プレイグラウンド

<メモ開始>
Kotlin プレイグラウンドを使えばKotlinの動作確認とか手軽に行えるのか!めっちゃ便利!
<メモ終了>

スレッドは、プログラム内でスケジュール設定して実行できるコードの最小単位です。同時実行コードを実行できる簡単な例を次に示します。

<メモ開始>

スレッドは、プログラム内でスケジュール設定して実行できるコードの最小単位です。

このスレッドの説明はなんだかよく分からないですね。先ほどのスレッドの説明ページではざっくりの理解っぽかったので、スレッドをもっと深掘ると分かってきそうですね、、、ここでは一旦飛ばします。
<メモ終了>

簡単なスレッドを作成するにはラムダを指定します。プレイグラウンドで以下を試してみましょう。

fun main() {
    val thread = Thread {
        println("${Thread.currentThread()} has run.")
    }
    thread.start()
}

<メモ開始>
KotlinではThread {}で、先ほどのスレッドの説明ページでいう処理の流れ(処理の線)を新たに生み出せるっぽいですね。
<メモ終了>

スレッドは、関数が start() 関数呼び出しに達するまで実行されません。出力結果は次のようになります。

Thread[Thread-0,5,main] has run.

なお currentThread() は、スレッドの名前、優先度、スレッド グループを返す文字列表現に変換された Thread インスタンスを返します。この出力は多少異なる場合があります。

<メモ開始>

currentThread() は、スレッドの名前、優先度、スレッド グループを返す文字列表現に変換された Thread インスタンスを返します。

Thread[Thread-0,5,main]

Threadをprintlnすると作成したスレッドの詳細が見れるんですね。出力の左から、スレッドの名前(Thread-0)、スレッドの優先度(5)、スレッドグループ(main)を表しているとのことですが、スレッドグループのみなんなのかよく分からないですね。
公式のThreadのリファレンスにはgetThreadGroup()というメソッドがあり、その戻り値はThreadGroupなので、ここを見てみると下記のようなスレッドグループの説明がありました。

A thread group represents a set of threads. In addition, a thread group can also include other thread groups.

<翻訳>
スレッドグループはスレッドの集合を表す。さらに、スレッドグループは他のスレッドグループを含むこともできる。

うん、名前のままですね。今回のThread[Thread-0,5,main]の場合だと、printlnしたスレッドはmainという名前のスレッドグループ(これがメインスレッドとかってやつ?)の中のスレッドであるという意味っぽいですね。
<メモ終了>

複数スレッドの作成と実行

単純な同時実行の例を示すため、スレッドをいくつか作成して実行してみましょう。このコードは、先ほどの例の情報行を出力する 3 つのスレッドを作成します。

fun main() {
   val states = arrayOf("Starting", "Doing Task 1", "Doing Task 2", "Ending")
   repeat(3) {
       Thread {
           println("${Thread.currentThread()} has started")
           for (i in states) {
               println("${Thread.currentThread()} - $i")
               Thread.sleep(50)
           }
       }.start()
   }
}

プレイグラウンドでの出力:

Thread[Thread-2,5,main] has started Thread[Thread-2,5,main] - Starting Thread[Thread-0,5,main] - Doing Task 1 Thread[Thread-1,5,main] - Doing Task 1 Thread[Thread-2,5,main] - Doing Task 1 Thread[Thread-0,5,main] - Doing Task 2 Thread[Thread-1,5,main] - Doing Task 2 Thread[Thread-2,5,main] - Doing Task 2 Thread[Thread-0,5,main] - Ending Thread[Thread-2,5,main] - Ending Thread[Thread-1,5,main] - Ending Thread[Thread-0,5,main] has started
Thread[Thread-0,5,main] - Starting
Thread[Thread-1,5,main] has started
Thread[Thread-1,5,main] - Starting

AS(コンソール)での出力:

Thread[Thread-0,5,main] has started
Thread[Thread-1,5,main] has started
Thread[Thread-2,5,main] has started
Thread[Thread-1,5,main] - Starting
Thread[Thread-0,5,main] - Starting
Thread[Thread-2,5,main] - Starting
Thread[Thread-1,5,main] - Doing Task 1
Thread[Thread-0,5,main] - Doing Task 1
Thread[Thread-2,5,main] - Doing Task 1
Thread[Thread-0,5,main] - Doing Task 2
Thread[Thread-1,5,main] - Doing Task 2
Thread[Thread-2,5,main] - Doing Task 2
Thread[Thread-0,5,main] - Ending
Thread[Thread-2,5,main] - Ending
Thread[Thread-1,5,main] - Ending

コードを複数回実行します。さまざまな出力が表示されます。スレッドが順番に実行されているように見える場合もあれば、コンテンツが散在しているように見える場合もあります。

注: この不変性は、スレッドの実行方法によって発生します。スケジューラは各スレッドにタイムスライスを渡し、その期間内に完了するか、別のタイムスライスを受け取るまで中断します。

<メモ開始>
このコードではThread {}で、スレッドの説明ページでいう処理の流れ(処理の線)をrepeat(3)で新たに3つ生み出してるっぽいですね。出力されたスレッドの中身のスレッドの名前の部分(Thread-x)を見ると、Thread-0 Thread-1 Thread-2と3種類の別のスレッドが生成されたことが分かります。

スレッドが順番に実行されているように見える場合もあれば、コンテンツが散在しているように見える場合もあります。

今回のコードは同時実行(ノンブロッキング)の例なので、これはThread-0 Thread-1 Thread-2がrepeat内で順番に実行されるのではなく同時実行されているからでしょうか。(Thread-0 Thread-1 Thread-2が下図のTask1, Task2, Task3のようになっている?)そのため、"${Thread.currentThread()} has started"の出力順番がバラバラになってしまっていると思われます。
image.png
また、各スレッドのfor内のThread.sleep(50)でスレッドを止めながら処理を行っているので、各スレッドごとのstates内の文字列出力順は順番通りになると思いきや、それさえも順番通りになっていません(states内のStarting → Doing Task 1 → Doing Task 2 → Endingの順番にならない)。

// Thread-0の出力順番を抜粋
// 実行毎に順番が変化します
Thread[Thread-0,5,main] - Doing Task 1
Thread[Thread-0,5,main] - Doing Task 2
Thread[Thread-0,5,main] - Ending Thread
Thread[Thread-0,5,main] - Starting

これはThread.sleep()の性質の影響であると思われます。(Thread.sleep())[https://developer.android.com/reference/kotlin/java/lang/Thread#sleep]のリファレンスを見ると下記のように書かれています。

Causes the currently executing thread to sleep (temporarily cease execution) for the specified number of milliseconds, subject to the precision and accuracy of system timers and schedulers.

<翻訳>
現在実行中のスレッドを、システムのタイマーとスケジューラの精度と正確さに従って、指定されたミリ秒数だけスリープ(一時的に実行を停止)させる。

現在実行中のスレッドを停止させるとあるため、同時実行しているThread-0 Thread-1 Thread-2の全てのスレッドを停止させているからだと推測できます。Thread.sleep()が全てのスレッドに作用するため、最終的な出力すべての順番がグチャグチャになっていそうですね。

ちなみに

AS(コンソール)での出力:

では、Starting → Doing Task 1 → Doing Task 2 → Endingの順番が順番通りになっているのですが、これは謎ですね(笑)。自分はめんどくさくてKotlin プレイグラウンドでしか試していないですが、Kotlin プレイグラウンドでは毎回出力順番は変わるんですよね。添付されているコンソールの出力ではたまたま順番が同じになったのでしょうか、あんまよくわかんないですね(笑)。
<メモ終了>

3.スレッドでの課題

スレッドを使用することは、複数のタスクと同時実行を扱う簡単な方法ですが、問題がないわけではありません。コードで Thread を直接使用すると、さまざまな問題が発生する可能性があります。

スレッドは多くのリソースを必要とする

スレッドの作成、切り替え、管理は、システム リソースを消費し、同時に管理できるスレッド数の制限に時間がかかります。作成のコストが大幅に増えることがあります。

実行中のアプリには複数のスレッドがありますが、アプリごとに 1 つの専用スレッドがあり、具体的にはアプリの UI を担当します。このスレッドは通常、メインスレッドまたは UI スレッドと呼ばれます。

注: 場合によっては、UI スレッドとメインスレッドが異なることがあります。

<メモ開始>

アプリごとに 1 つの専用スレッドがあり、具体的にはアプリの UI を担当します。このスレッドは通常、メインスレッドまたは UI スレッドと呼ばれます。

1.準備 で調べた通り、やはりUIに関することを行うスレッドはメインスレッドであるというのは正しそうです。
<メモ終了>

このスレッドはアプリの UI を実行するためのものであるため、アプリをスムーズに動作させるには、メインスレッドのパフォーマンスを高めることが重要です。長時間実行タスクは完了するまでブロックされ、アプリが応答しなくなります。

<メモ開始>

メインスレッドのパフォーマンスを高めることが重要です。長時間実行タスクは完了するまでブロックされ、アプリが応答しなくなります。

よくメインスレッドを止めるなと言うのはこのことですよね。長い時間がかかる処理はメインスレッドでは行わず、Thread {}等で別のスレッドを新たに作成しそこで実行することが必要そうですね。
<メモ終了>

オペレーティング システムは、ユーザーに対する応答性を維持しようとして多くのことを行います。現在のスマートフォンは、毎秒 60 回から 120 回(最低でも 60 回)UI を更新しようとします。UI を準備して描画する時間は限られています(60 フレーム/秒で、1 回の画面更新にかかる時間が 16 ms 以下)。Android はフレームをドロップするか、追いつくために 1 つの更新サイクルを完了しようとして中止します。一部のフレームがドロップし変動することは正常ですが、多すぎるとアプリが応答しなくなります。

競合状態と予期しない動作

前述のとおり、スレッドは、プロセッサが複数のタスクを同時に処理するように見える方法の抽象化です。プロセッサが異なるスレッドで命令セットを切り替えるとき、スレッドが実行される正確な時間と、スレッドが一時停止するタイミングは制御不能です。スレッドを直接操作する場合、必ずしも予測可能な出力が得られるとは限りません。

たとえば次のコードは、簡単なループを使用して 1 から 50 までカウントしますが、この場合、カウントが増えるたびに新しいスレッドが作成されます。出力がどのようになるかを考えてから、コードを数回実行してみましょう。

fun main() {
   var count = 0
   for (i in 1..50) {
       Thread {
           count += 1
           println("Thread: $i count: $count")
       }.start()
   }
}

<メモ開始>

出力がどのようになるかを考えてから、コードを数回実行してみましょう。

せっかくなので予想してみましょう。

for (i in 1..50)でスレッド(Thread)を50個作成しstartで開始していますね。そして各スレッドの中でforのブロックの外で宣言したcountの値を+1しています。 forで順番にスレッド作成&開始しているので出力もその順番になり、下記のような出力になるように見えます。

Thread: 1 count: 1
Thread: 2 count: 2
Thread: 3 count: 3
Thread: 4 count: 4
Thread: 5 count: 5
(・・・略・・・)
Thread: 49 count: 49
Thread: 50 count: 50

しかし複数スレッドの作成と実行では、forrepeat内で複数のThread{}を生成し処理を実行すると、処理が順番に実行されるのではなく同時実行されることを確認しました。今回のコードも同様の形なので、各スレッドが同時に実行され出力結果が順番通りではなくグチャグチャになりそう感じがしますね。

// 順番がグチャグチャ
Thread: 2 count: 1
Thread: 5 count: 3
Thread: 4 count: 2
Thread: 1 count: 4
Thread: 5 count: 5
(・・・略・・・)
Thread: 48 count: 49
Thread: 49 count: 50

<メモ終了>

出力は予想したとおりだったでしょうか。毎回同じだったでしょうか。出力例を次に示します。

Thread: 50 count: 49 Thread: 43 count: 50 Thread: 1 count: 1
Thread: 2 count: 2
Thread: 3 count: 3
Thread: 4 count: 4
Thread: 5 count: 5
Thread: 6 count: 6
Thread: 7 count: 7
Thread: 8 count: 8
Thread: 9 count: 9
Thread: 10 count: 10
Thread: 11 count: 11
Thread: 12 count: 12
Thread: 13 count: 13
Thread: 14 count: 14
Thread: 15 count: 15
Thread: 16 count: 16
Thread: 17 count: 17
Thread: 18 count: 18
Thread: 19 count: 19
Thread: 20 count: 20
Thread: 21 count: 21
Thread: 23 count: 22
Thread: 22 count: 23
Thread: 24 count: 24
Thread: 25 count: 25
Thread: 26 count: 26
Thread: 27 count: 27
Thread: 30 count: 28
Thread: 28 count: 29
Thread: 29 count: 41
Thread: 40 count: 41
Thread: 39 count: 41
Thread: 41 count: 41
Thread: 38 count: 41
Thread: 37 count: 41
Thread: 35 count: 41
Thread: 33 count: 41
Thread: 36 count: 41
Thread: 34 count: 41
Thread: 31 count: 41
Thread: 32 count: 41
Thread: 44 count: 42
Thread: 46 count: 43
Thread: 45 count: 44
Thread: 47 count: 45
Thread: 48 count: 46
Thread: 42 count: 47
Thread: 49 count: 48

コードの内容とは異なり、最後のスレッドが最初に実行され、他のスレッドの一部が順序どおりに実行されていないように見えます。一部の反復処理の「count」を見ると、複数のスレッドの後に変更されていないことがわかります。さらに奇妙なことに、2 番目に実行するスレッドでしかないことが出力で示されているにもかかわらず、Thread 43 で count が 50 に達します。この出力だけでは、count の最終的な値を知ることはできません。

<メモ開始>
出力結果がグチャグチャになるという予想が当たりましたね!(笑)
出力が思ったよりグチャグチャになっていて驚きました!
<メモ終了>

これは、スレッドが予期しない動作を引き起こす可能性がある方法のひとつにすぎません。複数のスレッドを操作するとき、競合状態という状態になることもあります。これは、複数のスレッドがメモリ内の同じ値に同時にアクセスしようとした場合に発生します。競合状態は、再現の難しいランダムに見えるバグを引き起こす可能性があり、アプリの頻繁かつ予期しないクラッシュの原因になることがあります。

パフォーマンスの問題、競合状態、バグの再現が難しいことなどから、スレッドを直接操作することはおすすめしません。代わりに、同時実行コードを記述するために役立つ、コルーチンという Kotlin の機能について学びます。

<メモ開始>
このような同時実行する際のデメリットを解消できるのがコルーチンなんですね!すごい!
<メモ終了>

4.Kotlinのコルーチン

バックグラウンド タスク用のスレッドを直接作成して使用することは、Android では一定の役割を持っていますが、Kotlin には、同時実行をより柔軟かつ簡単に管理できるコルーチンもあります。

コルーチンによってマルチタスクが可能となりますが、単にスレッドを操作するだけではなく、別のレベルの抽象化が提供されます。コルーチンの主な特長のひとつは状態を保存できることです。そのため、停止と再開ができます。コルーチンは実行される場合と実行されない場合があります。

<メモ開始>
コルーチンって結局なんなの?あんまり説明がないまま進みそうな感じがしますね。
コルーチンのWikipediaには下記のような説明がありました。

コルーチン(英: co-routine)とはプログラミングの構造の一種。サブルーチンがエントリーからリターンまでを一つの処理単位とするのに対し、コルーチンはいったん処理を中断した後、続きから処理を再開できる。

サブルーチンがわからないので調べると、サブルーチンのWikipediaには、

サブルーチン(英: subroutine)はプログラム中で意味や内容がまとまっている作業をひとつにまとめたものである。サブプログラム (英: subprogram) 、副プログラム[1]とも呼ばれ、単に「ルーチン」(英: routine)とも呼ばれる。なお、オブジェクト指向プログラミング言語においてはクラスの「メソッド」と呼ばれる。

とありました。つまり、サブルーチンもコルーチンもどちらもプログラムのまとまりではあるが、コルーチンは途中で中断でき、続きから再開できる特徴を持つプログラムのまとまりであると言えそうです。
<メモ終了>

「継続」で表される状態によって、コードの一部は、制御を引き渡す必要がある場合、または別のコルーチンが処理を完了するまで待ってから再開する必要がある場合に通知できます。このフローを協調的マルチタスクといいます。Kotlin のコルーチン実装では、マルチタスクに役立つ多くの機能が追加されます。継続に加え、コルーチンの作成には CoroutineScope 内の Job(ライフサイクルを伴うキャンセル可能な作業単位)での作業も含まれます。CoroutineScope は、子とその子にキャンセルとその他のルールを再帰的に適用するコンテキストです。Dispatcher は、コルーチンが実行に使用するバッキング スレッドを管理します。これによりデベロッパーは、新しいスレッドをいつどこで使用するかを気にする必要がなくなります。
image.png
これらについては後で詳しく説明しますが、Dispatchers はコルーチンのパフォーマンスを向上させる方法のひとつです。新しいスレッドを初期化するパフォーマンス コストを回避できます。

これまでの例を、コルーチンを使用するように応用してみましょう。

import kotlinx.coroutines.*

fun main() {
    repeat(3) {
        GlobalScope.launch {
            println("Hi from ${Thread.currentThread()}")
        }
    }
}
Hi from Thread[DefaultDispatcher-worker-2@coroutine#2,5,main]
Hi from Thread[DefaultDispatcher-worker-1@coroutine#1,5,main]
Hi from Thread[DefaultDispatcher-worker-1@coroutine#3`,5,main]

上のスニペットでは、デフォルトの Dispatcher を使用して、Global Scope 内に 3 つのコルーチンを作成しています。GlobalScope は、アプリが動作している限り、その中に含まれる任意のコルーチンを実行できます。メインスレッドについて説明した理由により、これはサンプルコード以外ではおすすめしません。アプリでコルーチンを使用するときは、他のスコープを使用します。

<メモ開始>
CoroutineScopeの一種にGlobalScopeがあるようですね。このようなCoroutineScopeに.launch()を付けて呼び出すだけでコルーチンが作成できるんですね。
<メモ終了>

launch() 関数は、キャンセル可能な Job オブジェクトにラップされたコードからコルーチンを作成します。launch() は、コルーチンの範囲外で戻り値が必要ない場合に使用します。

次に重要なコルーチンのコンセプトを理解するために、launch() のシグネチャ全体を見てみましょう。

fun CoroutineScope.launch {
    context: CoroutineContext = EmptyCoroutineContext,
    start: CoroutineStart = CoroutineStart.DEFAULT,
    block: suspend CoroutineScope.() -> Unit
}

背後では、起動するために渡したコードブロックが suspend キーワードでマークされます。suspend は、コードまたは関数のブロックを一時停止または再開できることを伝えます。

<メモ開始>
つまりsuspend キーワードは、このプログラムはコルーチン(中断と再開が可能)ですよ〜!ということを伝えるタグみたいなもの?
<メモ終了>

runBlocking について

次の例では runBlocking() を使用します。これは名前のとおり、新しいコルーチンを開始し、完了するまで現在のスレッドをブロックします。これは主に、main 関数とテストで、ブロックするコードとブロックしないコードを橋渡しするために使用されます。一般的な Android コードではあまり使用しません。

import kotlinx.coroutines.*
import java.time.LocalDateTime
import java.time.format.DateTimeFormatter

val formatter = DateTimeFormatter.ISO_LOCAL_TIME
val time = { formatter.format(LocalDateTime.now()) }

suspend fun getValue(): Double {
    println("entering getValue() at ${time()}")
    delay(3000)
    println("leaving getValue() at ${time()}")
    return Math.random()
}

fun main() {
    runBlocking {
        val num1 = getValue()
        val num2 = getValue()
        println("result of num1 + num2 is ${num1 + num2}")
    }
}

<メモ開始>

次の例では runBlocking() を使用します。これは名前のとおり、新しいコルーチンを開始し、完了するまで現在のスレッドをブロックします。

CoroutineScopeに.launch()もコルーチンを作成するものだったと思うので、それと同じことがrunBlocking()でもできるのだろうか。これらの違いは現在のスレッドをブロックしてしまうのがrunBlocking()の方って感じ?
<メモ終了>

getValue() は、設定された遅延時間の経過後に乱数を返します。DateTimeFormatter を使用します。該当する entry と exit の時間を示します。main 関数は getValue() を 2 回呼び出し、合計を返します。

entering getValue() at 17:44:52.311
leaving getValue() at 17:44:55.319
entering getValue() at 17:44:55.32
leaving getValue() at 17:44:58.32
result of num1 + num2 is 1.4320332550421415

<メモ開始>
entering → leavingが2回出力されており、enteringとleavingの感覚が約3000ミリ秒(3秒)になっています。runBlocking()は現在のスレッドをブロックしてしまう(ブロッキングな処理?)。だから2回のgetValue()の処理順番が保たれているっぽいですね。
<メモ終了>

この動作を確認するには、main() 関数を次のコードに置き換えます(他のコードはすべて維持します)。

fun main() {
    runBlocking {
        val num1 = async { getValue() }
        val num2 = async { getValue() }
        println("result of num1 + num2 is ${num1.await() + num2.await()}")
    }
}

<メモ開始>
launchと並んでasyncもコルーチンを作成する関数としてこの章の序盤に紹介されていましたね!
<メモ終了>

getValue() の 2 つの呼び出しは独立しており、必ずしもコルーチンを中断する必要はありません。Kotlin には、launch に似た async 関数があります。async() 関数は次のように定義されます。

Fun CoroutineScope.async() {
    context: CoroutineContext = EmptyCoroutineContext,
    start: CoroutineStart = CoroutineStart.DEFAULT,
    block: suspend CoroutineScope.() -> T
}: Deferred<T>

async() 関数は Deferred 型の値を返します。Deferred は、将来の値への参照を保持できるキャンセル可能な Job です。Deferred を使用すると、すぐに値を返すかのように関数を呼び出すこともできますが、非同期タスクがいつ返されるかは不明なため、Deferred は単にプレースホルダとして機能します。Deferred(他の言語では Promise または Future ともいいます)は、値が後でこのオブジェクトに返されることを保証します。一方、非同期タスクは、デフォルトでは実行をブロックせず、待機もしません。現在のコード行が Deferred の出力を待機する必要があることを伝えるには、await() を呼び出します。未加工の値が返されます。

entering getValue() at 22:52:25.025
entering getValue() at 22:52:25.03
leaving getValue() at 22:52:28.03
leaving getValue() at 22:52:28.032
result of num1 + num2 is 0.8416379026501276

<メモ開始>
この出力結果を見てみると、entering → entering → leaving → leavingの順番になっていることが分かります。これはasync()を利用したことによって2つのgetValue()が同時実行されたからですよね。

先ほどのrunBlocking()の説明には

次の例では runBlocking() を使用します。これは名前のとおり、新しいコルーチンを開始し、完了するまで現在のスレッドをブロックします。

と記載があり、runBlocking()内に記載した処理は何がなんでも順番に実行されそうな感じがしましたが、内部でasync()を使えばその限りではないということなのでしょうか。
<メモ終了>

関数を suspend としてマークする場合

前の例では、getValue() 関数も suspend キーワードで定義されています。これは、suspend 関数でもある delay() を呼び出すためです。ある関数が別の suspend 関数を呼び出す場合、その関数も suspend 関数である必要があります。

この場合、この例の main() 関数が suspend でマークされないのはなぜでしょうか。結局 getValue() を呼び出します。

必ずしもそうとは限りません。実際には、getValue()runBlocking() に渡された関数の中で呼び出されます。これは launch()async() に渡されたものと同様の suspend 関数です。ただし、getValue()main() 自体では呼び出されておらず、runBlocking()suspend 関数ではないため、main()suspend でマークされません。suspend 関数を呼び出さない関数の場合、それ自体が suspend 関数である必要はありません。

参考文献

Codelab コルーチンの概要
「分かりそう」で「分からない」でも「分かった」気になれるIT用語辞典
Android Developers

3
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
3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?