概要
本記事は、MIXIのminimo負債返済チームでのインターン中に Swift Concurrency を触っていて
「多分こうだろうな」と思いながら使っていた部分やよく見かけるものを、自分の備忘録として整理したものです。
忘れないためのメモですが、同じように「ふわっとした理解で使っている」人の参考になればうれしいです。
async/await
- async: 非同期処理を含む関数につけるマーク
- await: 「結果が返ってくるまで待つ」合図
Task の中で async を使う場合
同期コード(普通の関数や SwiftUI の onAppear
など)から async
を呼ぶ場合は、
直接は呼べないので Task {}
を使って「非同期の世界」に入ります。
func fetchUserName() async -> String {
return "Erika"
}
Task {
let name = await fetchUserName()
print(name)
}
async の中で async を使う場合
すでに async
な関数の中では、他の async 関数をそのまま await
で呼べます。
非同期処理同士は自然につながるイメージです。
func fetchUserName() async -> String {
return "Erika"
}
func showUserName() async {
let name = await fetchUserName()
print(name)
}
Task
同期コードから async を呼び出すための「非同期処理の入口」。
Task {}
の中は async コンテキストになるので、await
が使える。
Task {
await doSomething()
}
print("Taskを作った瞬間に次の処理に進む")
Task は非同期処理の入口
非同期処理の呼び出しをたどっていくと、必ずどこかで Task {}
に行き着きます。
同期の世界(ボタン押下や onAppear など)から async の世界に入るための「橋渡し」として使われます。
同期コード ──▶ Task { ... } ──▶ async関数 ──▶ await ...
実際の例(SwiftUI の onAppear から async を呼ぶ)
struct ContentView: View {
var body: some View {
Text("Hello")
.onAppear {
// onAppear は同期コンテキスト
// 直接 await は書けない
Task {
let name = await fetchUserName()
print("ユーザー名: \(name)")
}
}
}
}
func fetchUserName() async -> String {
return "Erika"
}
Sendable
Sendable
は「この型は並行処理で安全に渡せますよ」という意味を持つプロトコルです。
タスク間でデータをやり取りするときに、データ競合が起きないようにチェックしてくれます。
競合が起こると何がまずいの?
複数のタスクが同時に同じデータを書き換えると、結果が保証されません。
Sendable は「その型をタスク間で安全に受け渡せるかどうか」をチェックします。
このとき、値型(struct)と参照型(class)では扱いが異なります。
class Counter {
var value = 0
}
let counter = Counter()
Task { counter.value += 1 }
Task { counter.value += 1 }
2 になることを期待していますが、実際には同時アクセスのタイミングによって 1 のままになることがあります。
こうした問題を防ぐために Sendable が導入されています。
Sendable と Actor の違い(ざっくり)
-
Sendable = 型の「安全証明書」
→ 「このデータはタスク間で安全に渡せます」とコンパイル時に保証してくれる仕組み。 -
Actor = 「安全な部屋」
→ 複数タスクから同時にアクセスされても、順番に処理してデータが壊れないように守ってくれる仕組み。
どちらもデータレースを防ぐためのものですが、
- Sendable は 受け渡し時のルール(静的チェック)
- Actor は 実際のアクセス制御(実行時制御)
という違いがあります。
※データレースとは、複数のタスクやスレッドが 同じ変数(共有状態)を同時に読み書きすること で、結果が不定になる現象。
Actor
複数のタスクから同時にアクセスされても、ひとつずつ順番に処理する仕組みを持っています。
そのためデータ競合を防ぐことができます。
普通のクラスだと危険
class Counter {
var value = 0
func increment() { value += 1 }
}
let counter = Counter()
Task { counter.increment() }
Task { counter.increment() }
同時に呼ばれると内部状態が壊れる可能性がある(値が飛ぶ)
actor を使うと安全
actor Counter {
private var value = 0
func increment() { value += 1 }
func current() -> Int { value }
}
let counter = Counter()
Task { await counter.increment() }
Task { await counter.increment() }
- actor の中は「1人ずつ順番待ち」になる
- そのため同時アクセスしてもデータが壊れない
- メソッド呼び出し時に await が必要なのは「中に入れるまで待つ」から
その他(私が勘違いしていたこと)
非同期の中に非同期?
最初は「非同期の中でさらに非同期を待つってどういうこと?」と思っていました。
でも実際は API → 画像ロード → UI 更新 みたいに、処理を順番につなげていきます。
Task {
let users = try await fetchUsers() // API呼び出し
let avatars = try await fetchAvatars(users) // さらに非同期処理
await MainActor.run {
self.images = avatars // UI更新
}
}
並列 = スレッドがたくさん走っている?
これも最初に勘違いしていました。
「並列だから処理ごとに新しいスレッドが生まれてる」と思っていたけど実際は違います。
- Swift Concurrency では タスクを効率よく少数のスレッドに割り当てて動かしている
- スレッドは有限で、OS や Swift がうまくスケジューリングしてくれる
「並列 = スレッドが無限に増える」ではなく、「タスクが同時進行しているように見える」仕組み。
終わりに
MIXIのインターンでたくさん Swift Concurrency を触る中で、
「ふわっと理解していたものが整理されて、知見が増えたな」と実感しています。
今後また見返して役立てたいです。
参考:
https://www.swiftlangjp.com/language-guide/concurrency.html
https://developer.apple.com/documentation/swift/concurrency
https://zenn.dev/koher/articles/swift-concurrency-cheatsheet