📚

【Swift Concurrency】@TaskLocalについて

2024/06/24に公開

概要

@TaskLocalは、特定のコンテキストごとにローカルな値を保持するためのプロパティラッパーです。

基本的に以下のような形で定義します。@TaskLocalは、staticプロパティとして定義される必要があり、尚且つデフォルト値をもつ必要があります。

enum Sample {
    @TaskLocal static var value: String?
}

SwiftUIの@Environmentに似ています。例外を除きタスクローカル値は、特定のタスク内で作成された全て子タスクからアクセスできます。(例外は注意点として後述します。)

使い方

タスクローカル値(TaskLocal<Value>)がもつwithValueで値を設定し、そのスコープ内で設定した値を使用するといった形です。コンテキストの作成と捉えていただいても差し支えないと思います。

Swift Concurrencyのキャッチアップ中に@TaskLocalの存在を知り、本記事に辿り着いたという方も多いと思いますが、@TaskLocalに関しては、非同期コンテキスト(Task内やrefreshable内)でなくても使用できます。

サンプル1

以下のサンプルコードでは、MyTaskLocal.$value.withValue("🟥")でタスクローカル値が"🟥"に設定されているコンテキストを作成し、その中でactionを呼び出しています。actionMyTaskLocal.valueを出力します。呼び出したコンテキストでは、MyTaskLocal.value"🟥"に設定されているため、Optional("🟥")が出力されます。

enum MyTaskLocal {
    @TaskLocal static var value: String?
}

func action() {
    print(MyTaskLocal.value as Any)
}

MyTaskLocal.$value.withValue("🟥") {
    action()
}

// 出力結果
// Optional("🟥")

サンプル2

タスクローカル値は設定後、特定のタイミング(階層)で値を変更することもできます。

enum MyTaskLocal {
    @TaskLocal static var value: String?
}

func action(_ prefix: String) {
    print(prefix, MyTaskLocal.value as Any)
}

MyTaskLocal.$value.withValue("🟥") {
    action("①")
    MyTaskLocal.$value.withValue("🟦") {
        action("②")
    }
}

// 出力結果
// ① Optional("🟥")
// ② Optional("🟦")

サンプル3

タスクローカル値が設定されていないコンテキストで、タスクローカス値にアクセスした場合は、デフォルト値が返ります。

enum MyTaskLocal {
    @TaskLocal static var value: String?
}

func action() {
    print(MyTaskLocal.value as Any)
}

action()

// 出力結果
// nil

サンプル4

以下のサンプルコードでは、MyTaskLocal.value
"🟥"が設定されているコンテキスト、
"🟦"が設定されているコンテキスト、
"🟨"が設定されているコンテキスト
を作成し、actionを実行しています。

actionMyTaskLocal.valueを出力しますが、それぞれのコンテキストでMyTaskLocal.valueの値は異なるので、以下のような出力結果となります。

enum MyTaskLocal {
    @TaskLocal static var value: String?
}

Task {
    await withDiscardingTaskGroup { group in
        for memberName in ["🟥", "🟦", "🟨"] {
            group.addTask {
                MyTaskLocal.$value.withValue(memberName) {
                    action()
                }
            }
        }
    }
}

func action() {
    print(MyTaskLocal.value as Any)
}

// 出力結果
// Optional("🟥")
// Optional("🟦")
// Optional("🟨")

サンプル5

先ほどのコードでは、コンテキスト内で同期的な関数actionを呼び出していました。タスクローカル値を設定したコンテキストで非同期関数を呼ぶ場合は、非同期関数として定義されているwithValueを使用します。(先ほどのコードにawaitをつけるだけです。)

withValue(同期)
@discardableResult
final func withValue<R>(
    _ valueDuringOperation: Value,
    operation: () throws -> R,
    file: String = #fileID,
    line: UInt = #line
) rethrows -> R
withValue(非同期)
@backDeployed(before: macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0)
@discardableResult
final func withValue<R>(
    _ valueDuringOperation: Value,
    operation: () async throws -> R,
    isolation: isolated (any Actor)? = #isolation,
    file: String = #fileID,
    line: UInt = #line
) async rethrows -> R
enum MyTaskLocal {
    @TaskLocal static var value: String?
}

Task {
    await withDiscardingTaskGroup { group in
        for memberName in ["🟥", "🟦", "🟨"] {
            group.addTask {
                await MyTaskLocal.$value.withValue(memberName) {
                    await asynchronousAction()
                }
            }
        }
    }
}

func asynchronousAction() async {
    try? await Task.sleep(for: .seconds(3))
    print(MyTaskLocal.value as Any)
}

// 出力結果(順序が異なることが多い)
// Optional("🟥")
// Optional("🟦")
// Optional("🟨")

注意点

Taskun-structured(非構造化)となりますが、タスクローカル値を継承します。しかし、detachedで作成されたTaskに関しては、タスクローカル値を継承しません。

enum MyTaskLocal {
    @TaskLocal static var value: String?
}

MyTaskLocal.$value.withValue("🟥🟦🟨") {
    Task {
        print("①", MyTaskLocal.value as Any)
    }
    Task.detached {
        print("②", MyTaskLocal.value as Any)
    }
}

// 出力結果
// ① Optional("🟥🟦🟨")
// ② nil

公式ドキュメント

https://developer.apple.com/documentation/swift/tasklocal

Discussion