mcpField
A
axiom-swift-concurrency-ref

axiom-swift-concurrency-ref

github.com/charleswiltgen開発1.2k90 回インストール今日更新
リポジトリ

axiom-swift-concurrency-ref について

Swift の並行処理の構文や再利用可能なパターンを調べるための API リファレンススキルです。Actor、Sendable、タスク管理、キャンセル処理、よくある問題を扱います。

主な機能

  1. 01Actor の隔離と再入
  2. 02Sendable 適合パターン
  3. 03Task の作成とキャンセル
  4. 04タスク優先度とローカル値
  5. 05タイムアウトと保持サイクルのパターン
  6. 06並行処理の注意点

活用シーン

  1. 01Actor の隔離ルールを確認
  2. 02Sendable のコンパイルエラーを解消
  3. 03タスクのキャンセルを管理
  4. 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_CONCURRENCYminimal明示的な Sendable 注釈のみチェックします
SWIFT_STRICT_CONCURRENCYtargeted推論された Sendable とクロージャーをチェックします
SWIFT_STRICT_CONCURRENCYcomplete完全な厳格並行性(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 クイックリファレンス

タスクAPISwift のバージョン
分離された型を定義するactor MyActor { }5.5+
メインスレッドで実行する@MainActor5.5+
安全に共有できるとマークする: Sendable5.5+
クロージャを安全に共有できるとマークする@Sendable5.5+
並列タスク(固定)async let5.5+
並列タスク(動的)withTaskGroup5.5+
値をストリーム配信するAsyncStream5.5+
コールバックをブリッジするwithCheckedContinuation5.5+
キャンセルを確認するTask.checkCancellation()5.5+
タスクスコープの値@TaskLocal5.5+
分離をアサートするMainActor.assumeIsolated5.9+ (iOS 17+)
呼び出し元の分離をキャプチャする#isolation6.0+
ロックベースの同期Mutex6.0+ (iOS 18+)
結果を破棄するwithDiscardingTaskGroup5.9+ (iOS 17+)
所有権を移譲するsending パラメーター6.0+
強制的にバックグラウンドで実行する@concurrent6.2+
分離された適合extension: @MainActor Proto6.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

コメント