한국어 | English | 日本語
Webアプリケーションエンジニア (経験8.8年)
技術・開発
engineering
ウェブフロントエンドと バックエンド開発を扱います

コルーチン:スレッドとの違いとその特徴

非同期処理のためにスレッドを無限に増やす時代は過ぎ去りました。OSのスケジューリングに依存するスレッドとは異なり、プログラム内部で実行フローを制御し、リソースを効率的に利用するコルーチンが、なぜ現代プログラミングの必須パラダイムになったのかを探ります。
プロセスとスレッドのメモリ構造の違いから始め、コルーチンが「軽量スレッド」と呼ばれる理由をコンテキストスイッチングのコストの側面から分析します。さらに、協調的マルチタスクの原理とともに、Kotlinの`launch`、`async`、`runBlocking`などの主要なコルーチンビルダーの特徴と違いを学びます。

初めてKotlinを使用していた時に、非同期処理のためにコルーチンという概念に出会いました。同期とは、リクエストを送信した後、そのリクエストに対する戻り値を受け取るまで待機することを意味し、非同期とは、その待機時間中に他の作業を実行して効率を高めることを意味します。

同期と非同期は、「待機」が必要な処理が頻繁に登場するプログラミングにおける概念であり、これらは「ブロッキング」と名付けられます。例えば、OSの授業で習ったI/O処理やネットワークのリクエスト/レスポンス処理などが挙げられます。以前は、前述のような例を処理する際にのみ非同期が使用されていたと記憶していますが、現在ではどのような作業でも細かく分割して非同期で処理されるようになっています。このような傾向を促進しているのは、使いやすさが向上したことでしょう。ここで説明するコルーチンの概念も、スレッドよりも非同期処理を簡単に使えるようにしているためではないかと思います。

プロセスとスレッド

プロセス: プログラムがメモリにロードされ、実行されるインスタンス
スレッド: プロセス内で実行される複数のフローの単位

まず、スレッドはプロセスよりも小さい実行インスタンスであると認識されていますが、メモリ領域も少し異なります。

プロセスとスレッドのメモリ割り当てを示す図。プロセスにはヒープ、各スレッドにはスタックが割り当てられている。

プロセスは独立したメモリ領域(ヒープ)を割り当てられ、各スレッドも独立したメモリ領域(スタック)を割り当てられます。スレッドは本質的にプロセス内に属しているため、ヒープメモリ領域は当該プロセスに属するすべてのスレッドが共有できます。

プログラムに対するプロセスが生成されると、ヒープ領域と一つのスレッド、そして一つのスタック領域を持ちます。スレッドが追加されるたびに、その数だけスタックが追加されます。もし100個のスレッドがある場合、全体メモリに100個のスタックが生成されることになります。

並行性と並列性

並行性 (Concurrency)

インターリービング (時分割): 複数のタスクがある場合に、各タスクを平等に少しずつ分けて実行すること。

シングルコアで3つのタスクが順次インターリーブされて実行される様子を示す図。

総実行時間は、コンテキストスイッチングのコストを除けば、各タスクの実行時間を合計した時間と同じになります。例えば、3つのタスクがそれぞれ10分かかると仮定すると、合計30分が必要となります。

並列性 (Parallelism)

並列実行: 複数のタスクが同時に実行されること。

複数コアで3つのタスクが並列に実行される様子を示す図。

タスクの数だけリソースが必要であり、コンテキストスイッチングは不要です。総実行時間は、複数のタスクの中で最も時間がかかるタスクの実行時間と同じになります。例えば、3つのタスクがそれぞれ10分、11分、12分かかると仮定すると、合計12分が必要となります。

スレッドとコルーチン

スレッドとコルーチンはともに、並行性 (Concurrency) (インターリービング) を保証するための技術です。複数の処理を同時に実行する際、スレッドは各処理に対応するメモリ領域を割り当てますが、複数の処理を同時に実行する必要があるため、OSレベルで各処理をどれだけ分配して実行すれば効率的かを決定するためにプリエンプティブスケジューリングが必要です。つまり、タスクAを少し、タスクBを少し、というように実行し、最終的にタスクAとタスクBの両方を達成します。一方、コルーチンは軽量スレッドと呼ばれます。これもまた、処理を効率的に分配して少しずつ実行し完遂する並行性を目指しますが、各処理に対してスレッドを割り当てるのではなく、小さなオブジェクトのみを割り当て、これらのオブジェクトを自在に切り替えることで、スイッチングコストを最大限に削減しています。

スレッド

シングルCPUコア上で異なるスレッド間のコンテキストスイッチングを示す図。

上記の図では、すべてのタスクがスレッド単位であることがわかります。スレッドAがタスク1を実行中にタスク2が必要になった場合、これを非同期で呼び出します。タスク1は進行中の作業を中断し(ブロックされ)、タスク2はスレッドBで実行されます。この時、CPUが演算のために参照するメモリ領域がスレッドAからスレッドBに切り替わるコンテキストスイッチングが発生します。タスク2が完了すると、その結果値がタスク1に返され、同時に実行されるタスク3とタスク4はそれぞれスレッドCとスレッドDに割り当てられます。シングルコアCPUは同時演算が不可能なため、この場合もOSカーネルのプリエンプティブスケジューリングによって、各タスク1、3、4をどれだけ実行し、中断し、次のタスクを実行するかを決定し、それに合わせて3つのタスクを交代で実行することで並行性を保証します。

コルーチン

単一のスレッド上で複数のコルーチンがOSレベルのコンテキストスイッチングなしで実行される様子を示す図。

作業の単位はコルーチンオブジェクトであるため、タスク1の実行中に非同期タスク2が発生しても、タスク1を実行していた同じスレッドでタスク2を実行できます。また、一つのスレッドで多数のコルーチンオブジェクトを実行することも可能です。上記の図に従い、タスク1とタスク2の切り替えは、単一のスレッドA上でコルーチンオブジェクトを交換するだけで行われるため、OSレベルのコンテキストスイッチングは不要です。一つのスレッドで多数のコルーチンを実行できること、そしてコンテキストスイッチングが不要であることから、コルーチンは軽量スレッドとも呼ばれます

ただし、上記の図のスレッドAとスレッドCの例のように、多数のスレッドが同時に実行される場合は、並行性を保証するために2つのスレッド間でのコンテキストスイッチングは実行されなければなりません。したがって、コルーチンを使用する際には、「コンテキストスイッチングなし」という利点を最大限に活用するために、多数のスレッドを使用するよりも、単一のスレッドで複数のコルーチンオブジェクトを実行することが推奨されます。

結局、コルーチンによって「作業」の単位がスレッドではなくオブジェクトに縮小されることで、 作業の切り替えや多数の作業の実行に、必ずしも多数のスレッドを必要としなくなります。


コルーチンはスレッドの代替ではなく、既存のスレッドをより細かく利用するための概念です。 一つのスレッドが多数のコルーチンを実行できるため、もはや作業の数だけスレッドを量産してメモリを消費する必要がありません。

スレッドとコルーチンの実行フローを示す比較進捗バー。コルーチンにおけるコンテキストスイッチングの削減を強調。

スレッドとコルーチンの例として示した図を上記のように要約しました。コルーチンを使用する場合、タスクが変わってもスレッドは維持されることがわかります。それに伴い、コンテキストスイッチングの回数も大幅に減少していることが見て取れます。コルーチンで説明したように、タスク3とタスク4もスレッドCではなくスレッドAで実行されるように設計すれば、コンテキストスイッチングが全くない設計も可能です。つまり、コルーチンが実行されるスレッドもプログラマーが共有スレッドプールを指定して決定するという意味であり、コルーチンを活用した効率性は、ひとえにプログラマーの力量にかかっているということです。

各言語のコルーチン

スタックフルとスタックレス

コルーチンについてさらに深く掘り下げると、スタックフルとスタックレスの二種類に分けられることがわかります。本記事の冒頭で述べたように、スレッドは独自のメモリ領域であるスタックを持ちます。スタックは関数実行順序を格納し、その管理を可能にします。軽量スレッドであるコルーチンのスタックフルとスタックレスは、コルーチンが独自のスタックを持つか持たないかを意味します。スタックフルコルーチンは、コルーチン内部で他の関数を呼び出した際、その関数内で現在のコルーチンをサスペンドできる(正確にはyieldを呼び出せる)ことを意味します。スタックレスコルーチンは、関数に対するスタックを別途持たないため、呼び出そうとする関数を改めてコルーチンオブジェクトでラップして「コルーチンをネストして呼び出す」ことで、以前のコルーチンと内部のコルーチンをサスペンドを介して接続する必要があります。

Kotlinコルーチン

buildSequence {}

fun g() = buildSequence {
  yield(1); yield(2);
}
for (v in g()) {
  println(v)
}

runBlocking {}

launch {}

async {}



コルーチン:スレッドとの違いとその特徴
Author
Aaron
Posted on
Licensed Under
CC BY-NC-SA 4.0
CC BY-NC-SA 4.0
同じカテゴリーの関連記事
最新記事
LLMフィルターが奪う会話の筋肉とコミュニケーション様式
会話における無礼さを濾過し、洗練された回答を生成するLLMツールが日常化した現代において、私たちは本当に思慮深い会話をしているのだろうか?リアルタイムのコミュニケーションにおける数多くの失敗を通じて磨かれるべき会話能力が、外部ツールに依存することで退化している現象と、それがもたらす社会的な不安や世代間の行動様式の変化について考察する。
シニア採用における年俸交渉の最適なタイミングと戦略
年俸交渉は単なる数字の交換ではなく、心理的な駆け引きとタイミングが重要です。本稿では、企業側にとって、候補者が計算的な態度を取りがちな最終合格後よりも、採用プロセスの初期段階から段階的に交渉を進めることが、なぜより効率的であり、率直な情報の共有に繋がるのかを考察します。
法治主義の限界と人間の多様性
全ての人間の行為を単一の法体系で規制できるという信念は、傲慢であるかもしれない。この記事は、中世の階層的な統制から脱却し、現代の無限の自由を手に入れた人類が直面する法治主義の逆説と、多様性という名のもとに深化する社会的強制力と他者への悪魔化現象を鋭く分析する。
토스트 예시 메세지