axiom-swift-concurrency-ref について
Swift の並行処理の構文や再利用可能なパターンを調べるための API リファレンススキルです。Actor、Sendable、タスク管理、キャンセル処理、よくある問題を扱います。
主な機能
- 01Actor の隔離と再入
- 02Sendable 適合パターン
- 03Task の作成とキャンセル
- 04タスク優先度とローカル値
- 05タイムアウトと保持サイクルのパターン
- 06並行処理の注意点
活用シーン
- 01Actor の隔離ルールを確認
- 02Sendable のコンパイルエラーを解消
- 03タスクのキャンセルを管理
- 04非同期処理にタイムアウトを追加
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ charleswiltgen/ axiom --skill axiom-swift-concurrency-refインストール後、Claude Code で/axiom-swift-concurrency-refを入力して読み込まれたか確認します。
詳細
Swift Concurrency API リファレンス
コピー&ペースト用のパターンと構文検索に役立つ、Swift Concurrency API の完全なリファレンスです。
axiom-swift-concurrency(並行処理をいつ、なぜ使うかを解説します。段階的なガイド、判断ツリー、@concurrent、分離された適合を扱います)を補完します。
関連スキル:axiom-swift-concurrency(段階的なガイド、判断ツリー)、axiom-synchronization(Mutex、ロック)、axiom-assume-isolated(assumeIsolated のパターン)
パート 1:Actor のパターン
Actor の定義
actor ImageCache {
private var cache: [URL: UIImage] = [:]
func image(for url: URL) -> UIImage? {
cache[url]
}
func store(_ image: UIImage, for url: URL) {
cache[url] = image
}
}
// Usage — must await across isolation boundary
let cache = ImageCache()
let image = await cache.image(for: url)
Actor のすべてのプロパティとメソッドは、デフォルトで分離されています。Actor の分離ドメイン外からアクセスする場合は、await を使用する必要があります。
Actor の分離ルール
各 Actor の保存プロパティとメソッドは、その Actor に分離されています。分離境界の外からアクセスするには await が必要です。これにより、Actor がリクエストを処理できるまで呼び出し元が一時停止します。
actor Counter {
var count = 0 // Isolated — external access requires await
let name: String // let constants are implicitly nonisolated
func increment() { // Isolated — await required from outside
count += 1
}
nonisolated func identity() -> String {
name // OK: accessing nonisolated let
}
}
let counter = Counter(name: "main")
await counter.increment() // Must await across isolation boundary
let id = counter.identity() // No await needed — nonisolated
nonisolated キーワード
変更されない状態に同期的にアクセスする場合は、分離をオプトアウトします。
actor MyActor {
let id: UUID // let constants are implicitly nonisolated
nonisolated var description: String {
"Actor \(id)" // Can only access nonisolated state
}
nonisolated func hash(into hasher: inout Hasher) {
hasher.combine(id) // Only nonisolated properties
}
}
nonisolated メソッドからは、分離された保存プロパティにアクセスできません。同期アクセスが必要なプロトコル適合(Hashable、CustomStringConvertible など)に使用します。
Actor のリエントランシー
Actor 内のサスペンドポイント(await)では、ほかの呼び出し元が処理に割り込めます。任意の 2 つの await 式の間に、状態が変わる可能性があります。
actor BankAccount {
var balance: Double = 0
func transfer(amount: Double, to other: BankAccount) async {
guard balance >= amount else { return }
balance -= amount
// REENTRANCY HAZARD: another caller could modify balance here
// while we await the deposit on the other actor
await other.deposit(amount)
}
func deposit(_ amount: Double) {
balance += amount
}
}
パターン:Actor 内で await を実行するたびに、その後で状態を再確認します。
actor BankAccount {
var balance: Double = 0
func transfer(amount: Double, to other: BankAccount) async -> Bool {
guard balance >= amount else { return false }
balance -= amount
await other.deposit(amount)
// Re-check invariants after await if needed
return true
}
}
グローバル Actor
グローバル Actor は、どこからでもアクセスできる単一の共有分離ドメインを提供します。
@globalActor
actor MyGlobalActor {
static let shared = MyGlobalActor()
}
@MyGlobalActor
func doWork() { /* isolated to MyGlobalActor */ }
@MyGlobalActor
class MyService {
var state: Int = 0 // Isolated to MyGlobalActor
}
@MainActor
UI 処理用の組み込みグローバル Actor です。UI の更新はすべて @MainActor 上で行う必要があります。
@MainActor
class ViewModel: ObservableObject {
@Published var items: [Item] = []
func loadItems() async {
let data = await fetchFromNetwork()
items = data // Safe: already on MainActor
}
}
// Annotate individual members
class MixedService {
@MainActor var uiState: String = ""
@MainActor
func updateUI() {
uiState = "Done"
}
func backgroundWork() async -> String {
await heavyComputation()
}
}
サブクラスの継承:クラスが @MainActor の場合、そのすべてのサブクラスも分離を継承します。
Actor の init
Actor の初期化子は Actor に分離されていません。init から分離されたメソッドを呼び出すことはできません。
actor DataManager {
var data: [String] = []
init() {
// Cannot call isolated methods here
// self.loadDefaults() // ERROR: actor-isolated method in non-isolated init
}
// Use a factory method instead
static func create() async -> DataManager {
let manager = DataManager()
await manager.loadDefaults()
return manager
}
func loadDefaults() {
data = ["default"]
}
}
Actor の注意点一覧
| 注意点 | 症状 | 対処法 |
|---|---|---|
| Actor のリエントランシー | await の間に状態が変化する | await のたびに状態を再確認する |
| nonisolated から分離された状態にアクセスする | コンパイルエラー | nonisolated を削除するか、プロパティを nonisolated にする |
| 同期コンテキストから Actor メソッドを呼び出す | 「Expression is 'async'」 | Task {} でラップするか、呼び出し元を async にする |
| グローバル Actor の継承 | サブクラスが @MainActor を継承する | どのメソッドに分離が必要かを慎重に判断する |
| Actor の init は分離されていない | init で分離されたメソッドを呼び出せない | ファクトリメソッドを使用するか、init の後に値を設定する |
| Actor のプロトコル適合 | 「Non-isolated」適合エラー | プロトコルメソッドに nonisolated を使用するか、分離された適合を使用する(Swift 6.2 以降) |
| ViewModel に Actor を使用する | @Published が機能せず、UI の更新に await が必要になる | UI に関係するコードには @MainActor クラスを使用し、UI に関係しない共有状態にのみ Actor を使用する |
| Actor 内で GCD によるキュー切り替えを行う | 分離の保証が崩れ、スレッドの過剰な生成につながる | GCD を削除する — Actor の分離によって、すでにアクセスは直列化されています |
パート 2:Sendable のパターン
Sendable への自動適合
保存プロパティがすべて Sendable であれば、値型は Sendable です。
// Structs: Sendable when all stored properties are Sendable
struct UserProfile: Sendable {
let name: String
let age: Int
}
// Enums: Sendable when all associated values are Sendable
enum LoadState: Sendable {
case idle
case loading
case loaded(String) // String is Sendable
case failed(Error) // ERROR: Error is not Sendable
}
// Fix: use a Sendable error type
enum LoadState: Sendable {
case idle
case loading
case loaded(String)
case failed(any Error & Sendable)
}
@Sendable クロージャ
分離境界を越えて渡すクロージャには @Sendable が必要です。@Sendable クロージャは、変更可能なローカル状態をキャプチャできません。
func runInBackground(_ work: @Sendable () -> Void) {
Task.detached { work() }
}
// All captured values must be Sendable
var count = 0
runInBackground {
// ERROR: capture of mutable local variable
// count += 1
}
let snapshot = count
runInBackground {
print(snapshot) // OK: let binding of Sendable type
}
@unchecked Sendable
スレッドセーフであることを手動で保証します。独自に同期を行う場合にのみ使用してください。
final class ThreadSafeCache: @unchecked Sendable {
private let lock = NSLock()
private var storage: [String: Any] = [:]
func get(_ key: String) -> Any? {
lock.lock()
defer { lock.unlock() }
return storage[key]
}
func set(_ key: String, value: Any) {
lock.lock()
defer { lock.unlock() }
storage[key] = value
}
}
@unchecked Sendable の要件
- クラスが
finalであること - 変更可能な状態がすべて同期プリミティブ(ロック、キュー、Mutex)で保護されていること
- 正しさの保証は開発者の責任です。コンパイラによるチェックは行われません
条件付き適合
struct Box<T> {
let value: T
}
// Box is Sendable only when T is Sendable
extension Box: Sendable where T: Sendable {}
// Standard library uses this extensively:
// Array<Element>: Sendable where Element: Sendable
// Dictionary<Key, Value>: Sendable where Key: Sendable, Value: Sendable
// Optional<Wrapped>: Sendable where Wrapped: Sendable
sending パラメーター修飾子(SE-0430)
分離境界を越えて値の所有権を移転します。呼び出し元はその値へのアクセス権を手放します。
func process(_ value: sending String) async {
// Caller can no longer access value after this call
await store(value)
}
// Useful for transferring non-Sendable types when caller won't use them again
func handOff(_ connection: sending NetworkConnection) async {
await manager.accept(connection)
}
ビルド設定
Xcode で Sendable チェックの厳格さを制御します。
| 設定 | 値 | 動作 |
|---|---|---|
SWIFT_STRICT_CONCURRENCY | minimal | 明示的な Sendable 注釈のみチェックします |
SWIFT_STRICT_CONCURRENCY | targeted | 推論された Sendable とクロージャーをチェックします |
SWIFT_STRICT_CONCURRENCY | complete | 完全な厳格並行性(Swift 6 のデフォルト) |
Sendable の落とし穴一覧
| 落とし穴 | 症状 | 対処法 |
|---|---|---|
| クラスは Sendable に準拠できない | "Class cannot conform to Sendable" | final にして不変にするか、ロックを使用して @unchecked Sendable にします |
| クロージャーが Sendable でない型をキャプチャする | "Capture of non-Sendable type" | キャプチャ前に値をコピーするか、型を Sendable にします |
| プロトコルで Sendable を要求できない | ジェネリック制約が複雑になる | where T: Sendable を使用します |
| @unchecked Sendable がバグを隠す | 実行時のデータ競合 | ロックやキューによって安全性が保証される場合にのみ使用します |
| Array/Dictionary の条件付き準拠 | コレクションは Element が Sendable の場合のみ Sendable です | 要素の型が Sendable であることを確認します |
| Error が Sendable でない | "Type does not conform to Sendable" | any Error & Sendable または型付きエラーを使用します |
パート 3:タスク管理
Task { }
現在のアクターコンテキストと優先度を継承する、構造化されていないタスクを作成します。
// Inherits actor context — if called from @MainActor, runs on MainActor
let task = Task {
try await fetchData()
}
// Get the result
let result = try await task.value
// Get Result<Success, Failure>
let outcome = await task.result
Task.detached { }
コンテキストを継承しないタスクを作成します。アクターも優先度も継承しません。
Task.detached(priority: .background) {
// NOT on MainActor even if created from MainActor
await processLargeFile()
}
使用する場面:呼び出し元のアクターで実行してはならないバックグラウンド処理です。ほとんどの場合は Task {} を優先してください。Task.detached が必要になることはめったにありません。
Task のキャンセル
キャンセルは協調的に行われます。キャンセルの設定は要求であり、タスク側で確認して応答する必要があります。
let task = Task {
for item in largeCollection {
// Option 1: Check boolean
if Task.isCancelled { break }
// Option 2: Throw CancellationError
try Task.checkCancellation()
await process(item)
}
}
// Request cancellation
task.cancel()
Task.sleep
現在のタスクを指定時間だけ一時停止します。キャンセルに対応しており、スリープ中にキャンセルされると CancellationError をスローします。
// Duration-based (preferred)
try await Task.sleep(for: .seconds(2))
try await Task.sleep(for: .milliseconds(500))
// Nanoseconds (older API)
try await Task.sleep(nanoseconds: 2_000_000_000)
Task.yield
ほかのタスクが実行できるよう、自発的に実行を譲ります。長時間実行される同期ループで使用します。
for i in 0..<1_000_000 {
if i.isMultiple(of: 1000) {
await Task.yield()
}
process(i)
}
Task の優先度
| 優先度 | 使用例 |
|---|---|
.userInitiated | ユーザーの直接操作、結果が画面に表示される処理 |
.high | .userInitiated と同じ |
.medium | 指定がない場合のデフォルト |
.low | プリフェッチ、緊急性の低い処理 |
.utility | 進捗を表示する長時間の計算 |
.background | メンテナンス、クリーンアップ、時間に制約されない処理 |
Task(priority: .userInitiated) {
await loadVisibleContent()
}
Task(priority: .background) {
await cleanupTempFiles()
}
@TaskLocal
子タスクに自動的に伝播する、タスクスコープの値です。
enum RequestContext {
@TaskLocal static var requestID: String?
@TaskLocal static var userID: String?
}
// Set values for a scope
RequestContext.$requestID.withValue("req-123") {
RequestContext.$userID.withValue("user-456") {
// Both values available here and in child tasks
Task {
print(RequestContext.requestID) // "req-123"
print(RequestContext.userID) // "user-456"
}
}
}
// Outside scope — values are nil
print(RequestContext.requestID) // nil
伝播ルール:@TaskLocal の値は Task {} で作成された子タスクに伝播します。Task.detached {} には伝播しません。
タスクのタイムアウトパターン
タスクグループで競合させ、任意の非同期操作に期限を設定します。
func withTimeout<T: Sendable>(
_ duration: Duration,
operation: @Sendable @escaping () async throws -> T
) async throws -> T {
try await withThrowingTaskGroup(of: T.self) { group in
group.addTask { try await operation() }
group.addTask {
try await Task.sleep(for: duration)
throw TimeoutError()
}
guard let result = try await group.next() else {
throw TimeoutError()
}
group.cancelAll() // Cancel the loser — without this it keeps running
return result
}
}
group.cancelAll() は重要です。これがないと、負けたタスク(タイムアウトまたは操作のいずれか)は、グループのスコープが終了するまで実行を続けます。
タスクの強参照循環
タスクはクロージャーと同様に変数をキャプチャします。self を参照するタスクをプロパティに保持すると、強参照循環が発生します。
// ❌ Retain cycle: self → task → self
task = Task {
while true { await self.poll() }
}
// ✅ Weak capture breaks the cycle
task = Task { [weak self] in
while let self, !Task.isCancelled {
await self.poll()
}
}
ルール:Task をプロパティに保持する場合や、無限の非同期シーケンスを反復する場合は [weak self] を使用します。短時間で完了する Task では、強参照キャプチャを使用できます。
Swift 6 での Thread.current
Swift 6 の言語モードでは、非同期コンテキストから Thread.current を利用できません。
// ❌ Compiler error in Swift 6 mode
func check() async { print(Thread.current) }
// ✅ Workaround for debugging only
extension Thread {
static var currentThread: Thread { Thread.current }
}
正しさの根拠としてスレッド ID に依存しないでください。タスクは中断ポイントでスレッド間を移動します。代わりに、分離ドメインについて考えてください。
タスクの注意点一覧表
| 注意点 | 症状 | 対処法 |
|---|---|---|
| タスクがキャンセルされない | リソースリークが発生し、ビューが表示されなくなった後も処理が続く | タスクを保持し、deinit/onDisappear でキャンセルする |
| キャンセルを無視している | キャンセル後もタスクが最後まで実行される | ループ内で Task.isCancelled を確認し、checkCancellation() を使う |
| Task.detached でアクターのコンテキストが失われる | 「MainActor に隔離されていません」 | アクターの隔離が必要な場合は Task {} を使う |
| 保存された Task で self をキャプチャする | 参照サイクルが発生し、deinit が呼ばれない | 長期間実行されるタスクや保存されたタスクでは [weak self] を使う |
| async はバックグラウンドで実行されると思い込む | コードが呼び出し元のアクター上で実行される | @concurrent を使ってバックグラウンド実行を強制する |
| TaskLocal が伝播しない | detached タスクで値が nil になる | TaskLocal は子タスクにのみ伝播し、detached タスクには伝播しない |
| タスクの優先度逆転 | 優先度の低いタスクが優先度の高いタスクをブロックする | ほとんどの場合はシステムが処理します。高優先度のタスクから低優先度のタスクを await するのは避けてください |
| async コンテキストで Thread.current を使う | Swift 6 モードでコンパイルエラーになる | スレッドの識別に依存せず、隔離ドメインを使う |
パート 4:構造化並行処理
async let
固定数の操作を並列に実行します。スコープを抜ける際に、すべての async let バインディングが暗黙的に await されます。
async let images = fetchImages()
async let metadata = fetchMetadata()
async let config = loadConfig()
// All three run concurrently, await together
let (imgs, meta, cfg) = try await (images, metadata, config)
セマンティクス:1 つの async let がスローすると、ほかのタスクはキャンセルされます。囲んでいるスコープを抜ける前に、すべてのタスクが完了する(またはキャンセルされる)必要があります。
TaskGroup — 非スロー
スローするタスクがない場合に、動的な数のタスクを並列に実行します。
let results = await withTaskGroup(of: String.self) { group in
for name in names {
group.addTask {
await fetchGreeting(for: name)
}
}
var greetings: [String] = []
for await greeting in group {
greetings.append(greeting)
}
return greetings
}
TaskGroup — スローあり
スローする可能性があるタスクを、動的な数だけ並列に実行します。
let images = try await withThrowingTaskGroup(of: (URL, UIImage).self) { group in
for url in urls {
group.addTask {
let image = try await downloadImage(url)
return (url, image)
}
}
var results: [URL: UIImage] = [:]
for try await (url, image) in group {
results[url] = image
}
return results
}
withDiscardingTaskGroup(iOS 17+)
並行処理は必要でも、結果を収集する必要がない場合に使います。結果を保存しないため、通常の TaskGroup よりメモリ効率に優れています。
try await withThrowingDiscardingTaskGroup { group in
for connection in connections {
group.addTask {
try await connection.monitor()
// Results are discarded — useful for long-running services
}
}
// Group stays alive until all tasks complete or one throws
}
実践的なパターン — 複数の通知ストリームを統合する
extension NotificationCenter {
func notifications(named names: [Notification.Name]) -> AsyncStream<Void> {
AsyncStream { continuation in
let task = Task {
await withDiscardingTaskGroup { group in
for name in names {
group.addTask {
for await _ in self.notifications(named: name) {
continuation.yield()
}
}
}
}
continuation.finish()
}
continuation.onTermination = { _ in task.cancel() }
}
}
}
TaskGroup の制御
await withTaskGroup(of: Data.self) { group in
// Add tasks conditionally
group.addTaskUnlessCancelled {
await fetchData()
}
// Cancel remaining tasks
group.cancelAll()
// Wait without collecting
await group.waitForAll()
// Iterate one at a time
while let result = await group.next() {
process(result)
}
}
タスクツリーのセマンティクス
構造化並行処理はツリーを形成します。
- 親のキャンセルはすべての子をキャンセルします — タスクをキャンセルすると、すべての
async letと TaskGroup の子タスクがキャンセルされます - 子のエラーは親に伝播します — スローありのグループでは、子のエラーによって兄弟タスクがキャンセルされ、エラーが親へ伝播します
- 親が戻る前にすべての子が完了する必要があります — スコープは、キャンセルされた子も含めてすべての子の完了を待ちます
// If fetchImages() throws, fetchMetadata() is automatically cancelled
async let images = fetchImages()
async let metadata = fetchMetadata()
let result = try await (images, metadata)
構造化並行処理の注意点一覧表
| 注意点 | 症状 | 対処法 |
|---|---|---|
| async let が未使用 | 処理は実行されるが、結果が何も通知されず破棄される | すべての async let の結果を割り当てるか、withDiscardingTaskGroup を使う |
| TaskGroup によるメモリの蓄積 | タスクが 1 万件を超えるとメモリ使用量が増加する | 結果を到着順に処理し、すべてを収集しない |
| addTask 内で可変状態をキャプチャする | 「キャプチャした var の変更」 | let バインディングまたはアクターを使う |
| 部分的な失敗を処理しない | 一部のタスクは成功し、一部は失敗する | group.next() を使い、エラーを個別に処理する |
| ループ内の async let | コンパイルエラー — async let は固定位置で使う必要がある | 代わりに TaskGroup を使う |
| グループから早期に戻る | 残りのタスクが実行され続ける | 戻る前に group.cancelAll() を呼び出す |
パート 5:非同期シーケンス
AsyncStream
時間の経過に伴って値を生成する、スローしないストリームです。
let stream = AsyncStream<Int> { continuation in
for i in 0..<10 {
continuation.yield(i)
}
continuation.finish()
}
for await value in stream {
print(value)
}
AsyncThrowingStream
エラーで失敗する可能性があるストリームです。
let stream = AsyncThrowingStream<Data, Error> { continuation in
let monitor = NetworkMonitor()
monitor.onData = { data in
continuation.yield(data)
}
monitor.onError = { error in
continuation.finish(throwing: error)
}
monitor.onComplete = {
continuation.finish()
}
continuation.onTermination = { @Sendable _ in
monitor.stop()
}
monitor.start()
}
do {
for try await data in stream {
process(data)
}
} catch {
handleStreamError(error)
}
Continuation API
let stream = AsyncStream<Value> { continuation in
// Emit a value
continuation.yield(value)
// End the stream normally
continuation.finish()
// Cleanup when consumer cancels or stream ends
continuation.onTermination = { @Sendable termination in
switch termination {
case .cancelled:
cleanup()
case .finished:
finalCleanup()
@unknown default:
break
}
}
}
// For throwing streams
let stream = AsyncThrowingStream<Value, Error> { continuation in
continuation.yield(value)
continuation.finish() // Normal end
continuation.finish(throwing: error) // End with error
}
バッファリングポリシー
値が消費されるより速く生成される場合の動作を制御します。
// Keep all values (default) — memory can grow unbounded
let stream = AsyncStream<Int>(bufferingPolicy: .unbounded) { continuation in
// ...
}
// Keep oldest N values, drop new ones when buffer is full
let stream = AsyncStream<Int>(bufferingPolicy: .bufferingOldest(100)) { continuation in
// ...
}
// Keep newest N values, drop old ones when buffer is full
let stream = AsyncStream<Int>(bufferingPolicy: .bufferingNewest(100)) { continuation in
// ...
}
| ポリシー | 動作 | 使用する状況 |
|---|---|---|
.unbounded | すべての値を保持する | コンシューマーが処理に追いつく場合、またはプロデューサーの生成数が制限されている場合 |
.bufferingOldest(N) | バッファーが満杯になると新しい値を破棄する | 順序が重要で、古い値を優先する場合 |
.bufferingNewest(N) | バッファーが満杯になると古い値を破棄する | 最新の状態が重要な場合(UI の更新、センサーデータなど) |
カスタム AsyncSequence
struct Counter: AsyncSequence {
typealias Element = Int
let limit: Int
struct AsyncIterator: AsyncIteratorProtocol {
var current = 0
let limit: Int
mutating func next() async -> Int? {
guard current < limit else { return nil }
defer { current += 1 }
return current
}
}
func makeAsyncIterator() -> AsyncIterator {
AsyncIterator(limit: limit)
}
}
// Usage
for await number in Counter(limit: 5) {
print(number) // 0, 1, 2, 3, 4
}
AsyncSequence の演算子
標準の演算子は、あらゆる AsyncSequence で使えます。
// Map
for await name in users.map(\.name) { }
// Filter
for await adult in users.filter({ $0.age >= 18 }) { }
// CompactMap
for await image in urls.compactMap({ await tryLoadImage($0) }) { }
// Prefix
for await first5 in stream.prefix(5) { }
// first(where:)
let match = await stream.first(where: { $0 > threshold })
// Contains
let hasMatch = await stream.contains(where: { $0 > threshold })
// Reduce
let sum = await numbers.reduce(0, +)
組み込みの非同期シーケンス
// NotificationCenter
for await notification in NotificationCenter.default.notifications(named: .didUpdate) {
handleUpdate(notification)
}
// URLSession bytes
let (bytes, response) = try await URLSession.shared.bytes(from: url)
for try await byte in bytes {
process(byte)
}
// FileHandle bytes
for try await line in FileHandle.standardInput.bytes.lines {
process(line)
}
非同期シーケンスで注意すべき点
| 注意点 | 症状 | 対処法 |
|---|---|---|
| 終了後に継続が値を返す | ランタイム警告が発生し、値が失われます | 終了状態を追跡し、値を返す前に確認します |
| ストリームが終了しない | for-await ループがいつまでも停止しません | すべてのコードパスで必ず continuation.finish() を呼び出します |
| onTermination ハンドラーがない | コンシューマーがキャンセルするとリソースリークが発生します | クリーンアップのために continuation.onTermination を設定します |
| バッファーに上限がない | 高負荷時にメモリが増加します | .bufferingNewest(N) または .bufferingOldest(N) を使用します |
| 複数のコンシューマー | 最初のコンシューマーだけが値を受け取ります | AsyncStream は単一コンシューマーです。コンシューマーごとに個別のストリームを作成します |
| MainActor 上の for-await | 値を待つ間、UI がフリーズします | Task {} を使ってメイン処理から離れて値を受け取ります |
パート 6:隔離パターン
関数に付ける @MainActor
@MainActor
func updateUI() {
label.text = "Done"
}
// Call from async context
func doWork() async {
let result = await computeResult()
await updateUI() // Hops to MainActor
}
MainActor.run
任意のコンテキストから、クロージャーを明示的にメインアクター上で実行します。
func processData() async {
let result = await heavyComputation()
await MainActor.run {
self.label.text = result
self.progressView.isHidden = true
}
}
MainActor.assumeIsolated(iOS 17 以降)
コードがすでにメインアクター上で実行されていることを表明します。表明が誤っている場合、ランタイムでクラッシュします。
func legacyCallback() {
// We KNOW this is called on main thread (UIKit guarantee)
MainActor.assumeIsolated {
self.viewModel.update() // Access @MainActor state
}
}
詳しいパターンについては、axiom-assume-isolated を参照してください。
nonisolated
外側のアクターによる隔離を解除します。
@MainActor
class ViewModel {
let id: UUID // Implicitly nonisolated (let)
nonisolated var analyticsID: String { // Explicitly nonisolated
id.uuidString
}
var items: [Item] = [] // Isolated to MainActor
}
nonisolated(unsafe)
コンパイラーの制約を回避する手段です。安全性の保証なしに、プロパティが隔離されていないものとして扱うようコンパイラーに指示します。
// Use only when you have external guarantees of thread safety
nonisolated(unsafe) var legacyState: Int = 0
// Common for global constants that the compiler can't verify
nonisolated(unsafe) let formatter: DateFormatter = {
let f = DateFormatter()
f.dateStyle = .medium
return f
}()
警告:nonisolated(unsafe) はランタイムでの保護を一切提供しません。データ競合は検出されません。レガシーコードとの接続における最終手段としてのみ使用してください。
@preconcurrency
移行中に、並行処理導入前の API に関する並行処理の警告を抑制します。
// Suppress warnings for entire module
@preconcurrency import MyLegacyFramework
// Suppress for specific protocol conformance
class MyDelegate: @preconcurrency SomeLegacyDelegate {
func delegateCallback() {
// No Sendable warnings for this conformance
}
}
#isolation(Swift 6.0 以降)
呼び出し元の隔離コンテキストを取得し、呼び出し元が属するアクター上で関数が実行されるようにします。
func doWork(isolation: isolated (any Actor)? = #isolation) async {
// Runs on caller's actor — no hop if caller is already isolated
performWork()
}
// Called from @MainActor — runs on MainActor
@MainActor
func setup() async {
await doWork() // doWork runs on MainActor
}
// Called from custom actor — runs on that actor
actor MyActor {
func run() async {
await doWork() // doWork runs on MyActor
}
}
Task クロージャー内での #isolation の取得(SE-0420)
Sendable ではない型を扱う必要がある Task クロージャーを生成するときは、隔離パラメーターを取得して、呼び出し元のコンテキストを継承します。
func process(
delegate: NonSendableDelegate,
isolation: isolated (any Actor)? = #isolation
) {
Task {
_ = isolation // Forces capture — Task inherits caller's isolation
delegate.doWork() // ✅ Safe: running on caller's actor
}
}
_ = isolation が必要な理由:SE-0420 によると、Task クロージャーが隔離パラメーターの隔離を継承するのは、そのパラメーターを非オプショナルなバインディングとしてクロージャー内で取得した場合のみです。_ = isolation 文によって、この取得を強制します。これがないと、Task はデフォルトのエグゼキューター上で実行され、Sendable ではないキャプチャーがコンパイラーエラーになります。
使用する場面:Sendable ではないデリゲートオブジェクトを扱う Task の生成、呼び出し元の状態へのアクセスが必要な fire-and-forget の非同期処理、デリゲートを保持しながらのコールバックベース API との接続に使用します。
隔離で注意すべき点
| 注意点 | 症状 | 対処法 |
|---|---|---|
| MainActor 上からの MainActor.run | 不要な切り替えが発生し、デッドロックのリスクがあります | コンテキストを確認するか、assumeIsolated を使用します |
| nonisolated(unsafe) によるデータ競合 | ランタイムでクラッシュしたり、状態が破損したりします | 適切な隔離または Mutex を使用します |
| @preconcurrency による実際の問題の隠蔽 | 本番環境でランタイムクラッシュが発生します | リリース前に適切な並行処理へ移行します |
| #isolation は 5.9 より前では使用不可 | コンパイラーエラーになります | 従来の @MainActor 注釈を使用します |
| Task 内で #isolation が取得されない | Sendable ではないキャプチャーのエラーになります | Task クロージャー内に _ = isolation を追加します(SE-0420) |
| アクターメソッドに nonisolated を指定 | 隔離された状態にアクセスできません | 隔離されていない状態からの計算プロパティにのみ使用します |
| 非同期コンテキストで Thread.current を使用 | Swift 6 モードではコンパイラーエラーになります | スレッドの識別には依存せず、隔離ドメインに基づいて考えます |
パート 7:継続
コールバックベースの API を async/await に接続します。
withCheckedContinuation
エラーをスローしない接続です。
func currentLocation() async -> CLLocation {
await withCheckedContinuation { continuation in
locationManager.requestLocation { location in
continuation.resume(returning: location)
}
}
}
withCheckedThrowingContinuation
エラーをスローする接続です。
func fetchUser(id: String) async throws -> User {
try await withCheckedThrowingContinuation { continuation in
api.fetchUser(id: id) { result in
switch result {
case .success(let user):
continuation.resume(returning: user)
case .failure(let error):
continuation.resume(throwing: error)
}
}
}
}
継続の再開メソッド
// Return a value
continuation.resume(returning: value)
// Throw an error
continuation.resume(throwing: error)
// From a Result type
continuation.resume(with: result) // Result<T, Error>
必ず一度だけ再開するルール
継続は必ず一度だけ再開しなければなりません。
- 2 回再開すると、
"Continuation already resumed"でクラッシュします(チェック付きの場合)。安全でない場合は、未定義の動作になります。 - 一度も再開しないと、待機中のタスクがいつまでも停止します。これは気付かれにくいリークです。
// DANGEROUS: callback might not be called
func riskyBridge() async throws -> Data {
try await withCheckedThrowingContinuation { continuation in
api.fetch { data, error in
if let error {
continuation.resume(throwing: error)
return
}
if let data {
continuation.resume(returning: data)
return
}
// BUG: if both are nil, continuation is never resumed
// Fix: add a fallback
continuation.resume(throwing: BridgeError.noResponse)
}
}
}
デリゲートとの接続
class LocationBridge: NSObject, CLLocationManagerDelegate {
private var continuation: CheckedContinuation<CLLocation, Error>?
private let manager = CLLocationManager()
func requestLocation() async throws -> CLLocation {
try await withCheckedThrowingContinuation { continuation in
self.continuation = continuation
manager.delegate = self
manager.requestLocation()
}
}
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
continuation?.resume(returning: locations[0])
continuation = nil // Prevent double resume
}
func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
continuation?.resume(throwing: error)
continuation = nil
}
}
Unsafe Continuations
パフォーマンスのため、実行時チェックを省略します。チェック付きのものと同じ API ですが、誤用した場合、診断付きのクラッシュではなく未定義の動作が発生します。
func fastBridge() async -> Data {
await withUnsafeContinuation { continuation in
// No runtime check for double-resume or missing resume
fastCallback { data in
continuation.resume(returning: data)
}
}
}
開発中はチェック付き継続を使用し、十分なテストを行った後、プロファイリングでチェックがボトルネックだと判明した場合にのみ、unsafe に切り替えてください。
継続に関する注意点の表
| 注意点 | 症状 | 対処法 |
|---|---|---|
| resume を2回呼び出す | 「Continuation already resumed」クラッシュ | resume の後に継続を nil に設定する |
| resume を一度も呼び出さない | タスクが永久に停止する | エラーや nil の場合も含め、すべてのコードパスで resume する |
| 継続をキャプチャする | 継続がスコープから漏れる | プロパティに保存し、resume が1回だけ呼び出されるようにする |
| デバッグ時の unsafe continuation | 誤用しても診断されない | 開発中は withCheckedContinuation を使用する |
| デリゲートが複数回呼び出される | 2回目の resume でクラッシュする | 繰り返し呼び出されるコールバックには、継続ではなく AsyncStream を使用する |
| コールバックが誤ったスレッドで呼び出される | 継続では問題にならない | 継続はどのスレッドからでも resume できる |
パート8:移行パターン
GCD や完了ハンドラーから Swift の並行処理への一般的な移行方法です。
DispatchQueue から Actor へ
// BEFORE: DispatchQueue for thread safety
class ImageCache {
private let queue = DispatchQueue(label: "cache", attributes: .concurrent)
private var cache: [URL: UIImage] = [:]
func get(_ url: URL, completion: @escaping (UIImage?) -> Void) {
queue.async { completion(self.cache[url]) }
}
func set(_ url: URL, image: UIImage) {
queue.async(flags: .barrier) { self.cache[url] = image }
}
}
// AFTER: Actor
actor ImageCache {
private var cache: [URL: UIImage] = [:]
func get(_ url: URL) -> UIImage? {
cache[url]
}
func set(_ url: URL, image: UIImage) {
cache[url] = image
}
}
DispatchGroup から TaskGroup へ
// BEFORE: DispatchGroup
let group = DispatchGroup()
var results: [Data] = []
for url in urls {
group.enter()
fetch(url) { data in
results.append(data)
group.leave()
}
}
group.notify(queue: .main) { use(results) }
// AFTER: TaskGroup
let results = await withTaskGroup(of: Data.self) { group in
for url in urls {
group.addTask { await fetch(url) }
}
var collected: [Data] = []
for await data in group {
collected.append(data)
}
return collected
}
use(results)
完了ハンドラーから async へ
// BEFORE
func fetchData(completion: @escaping (Result<Data, Error>) -> Void) {
URLSession.shared.dataTask(with: url) { data, _, error in
if let error { completion(.failure(error)); return }
guard let data else { completion(.failure(FetchError.noData)); return }
completion(.success(data))
}.resume()
}
// AFTER
func fetchData() async throws -> Data {
let (data, _) = try await URLSession.shared.data(from: url)
return data
}
@MainActor を使った @objc デリゲート
@MainActor
class ViewController: UIViewController, UITableViewDelegate {
// @objc delegate methods inherit @MainActor isolation from the class
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
// Already on MainActor — safe to update UI
updateSelection(indexPath)
}
}
NotificationCenter から AsyncSequence へ
// BEFORE
let observer = NotificationCenter.default.addObserver(
forName: .didUpdate, object: nil, queue: .main
) { notification in
handleUpdate(notification)
}
// Must remove observer in deinit
// AFTER
let task = Task {
for await notification in NotificationCenter.default.notifications(named: .didUpdate) {
await handleUpdate(notification)
}
}
// Cancel task in deinit — no manual observer removal needed
Timer から AsyncSequence へ
// BEFORE
let timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { _ in
updateUI()
}
// Must invalidate in deinit
// AFTER
let task = Task {
while !Task.isCancelled {
await updateUI()
try? await Task.sleep(for: .seconds(1))
}
}
// Cancel task in deinit
DispatchSemaphore から Actor へ
// BEFORE: Semaphore to limit concurrent operations
let semaphore = DispatchSemaphore(value: 3)
for url in urls {
DispatchQueue.global().async {
semaphore.wait()
defer { semaphore.signal() }
download(url)
}
}
// AFTER: TaskGroup with limited concurrency
await withTaskGroup(of: Void.self) { group in
var inFlight = 0
for url in urls {
if inFlight >= 3 {
await group.next() // Wait for one to finish
inFlight -= 1
}
group.addTask { await download(url) }
inFlight += 1
}
await group.waitForAll()
}
移行に関する注意点の表
| 注意点 | 症状 | 対処法 |
|---|---|---|
| DispatchQueue.sync から actor へ | デッドロックの可能性 | .sync を削除し、await を使用する |
| グローバルなディスパッチから actor への競合 | 直列化による速度低下 | Concurrency Instruments でプロファイリングする |
| レガシーなデリゲートと Sendable | 「Cannot conform to Sendable」 | @preconcurrency import または @MainActor による分離を使用する |
| コールバックが複数回呼び出される | 継続でクラッシュする | 継続ではなく AsyncStream を使用する |
| 非同期コンテキストでの Semaphore.wait | スレッド枯渇やデッドロックの可能性 | 手動で並行数を制限する TaskGroup を使用する |
| DispatchQueue.main.async から MainActor へ | 微妙なタイミングの違い | 同等の処理は MainActor.run です。境界ケースをテストしてください |
| 構造化タスクをトップレベルの Task に置き換える | キャンセルの伝播とエラー処理が失われる | 関連する並列処理には async let または TaskGroup を使用する |
| 警告の解消のために @unchecked Sendable を一括適用する | コードベース全体にある実際のデータ競合を隠してしまう | 適切な Sendable、actor、または sending を使い、一度に1つの型を修正する |
API クイックリファレンス
| タスク | API | Swift のバージョン |
|---|---|---|
| 分離された型を定義する | actor MyActor { } | 5.5+ |
| メインスレッドで実行する | @MainActor | 5.5+ |
| 安全に共有できるとマークする | : Sendable | 5.5+ |
| クロージャを安全に共有できるとマークする | @Sendable | 5.5+ |
| 並列タスク(固定) | async let | 5.5+ |
| 並列タスク(動的) | withTaskGroup | 5.5+ |
| 値をストリーム配信する | AsyncStream | 5.5+ |
| コールバックをブリッジする | withCheckedContinuation | 5.5+ |
| キャンセルを確認する | Task.checkCancellation() | 5.5+ |
| タスクスコープの値 | @TaskLocal | 5.5+ |
| 分離をアサートする | MainActor.assumeIsolated | 5.9+ (iOS 17+) |
| 呼び出し元の分離をキャプチャする | #isolation | 6.0+ |
| ロックベースの同期 | Mutex | 6.0+ (iOS 18+) |
| 結果を破棄する | withDiscardingTaskGroup | 5.9+ (iOS 17+) |
| 所有権を移譲する | sending パラメーター | 6.0+ |
| 強制的にバックグラウンドで実行する | @concurrent | 6.2+ |
| 分離された適合 | extension: @MainActor Proto | 6.2+ |
リソース
WWDC: 2021-10132, 2021-10134, 2022-110350, 2025-268
ドキュメント: /swift/concurrency, /swift/actor, /swift/sendable, /swift/taskgroup
スキル: swift-concurrency, assume-isolated, synchronization, concurrency-profiling