14.1 同期処理
一つの処理が終わるまで、次の処理へ進みません。
APIへ接続
応答を待つ
結果を処理
待機中に他の作業ができないため、I/O処理では効率が悪い場合があります。
14.2 非同期処理
処理の完了を待っている間に、別の処理を進めます。
const response = await fetch(url);
response = await client.get(url)
let response = client.get(url).send().await?;
asyncは、関数が即座に最終結果を返すのではなく、将来完了する処理を返すことを表します。
14.3 並行と並列
この二つは異なります。
並行
複数の処理を進行中の状態にすることです。
処理Aを少し進める
処理Bを少し進める
処理Aへ戻る
並列
複数の処理を物理的に同時実行することです。
CPUコア1で処理A
CPUコア2で処理B
非同期処理は並行性を実現しますが、必ずしも並列ではありません。
14.4 スレッド、タスク、コルーチン
| 概念 | 概要 |
|---|---|
| OSスレッド | OSが管理する実行単位 |
| 軽量スレッド | 言語ランタイムが管理する軽量な実行単位 |
| タスク | 非同期処理の論理的な単位 |
| コルーチン | 中断・再開できる関数 |
| Future・Promise | 将来得られる結果 |
Goのgoroutine、Kotlinのcoroutine、JavaScriptのPromise、RustのFutureなどは、同じ問題へ異なる抽象化を提供しています。
14.5 共有状態とメッセージパッシング
並行処理では、複数の処理が同じ状態を変更すると競合が起きます。
タスクAが残高を読む
タスクBが同じ残高を読む
両方が更新する
一方の更新が失われる
対処方法は大きく二つあります。
共有状態を同期する
Mutex
Lock
Atomic
Semaphore
状態を共有せずメッセージを送る
Channel
Actor
Message Queue
Goではチャネル、Erlang系ではActorモデル、Rustでもチャネルなどを利用できます。
14.6 構造化並行性
開始した非同期処理の寿命を、親処理のスコープへ結び付ける考え方です。
親タスク
├─ 子タスクA
└─ 子タスクB
親処理が終了するとき、子処理も完了・キャンセルされることを保証します。
目的は次です。
処理の取り残しを防ぐ
キャンセルを伝播させる
エラーをまとめて扱う
タスクの寿命を明確にする