Swift Digest
SE-0469 | Swift Evolution

Task Naming

Proposal
SE-0469
Authors
Konrad Malawski, Harjas Monga
Review Manager
Holly Borla
Status
Implemented (Swift 6.2) except for the amendment

01 何が問題だったのか

Swift Concurrencyでは、Task を大量に生成して非同期処理を走らせることができますが、これまで Task 自体に「どんな仕事をしているのか」を示す名前を付ける手段がありませんでした。デバッガやプロファイラ、swift-inspect のようなツールで実行中のタスクを観察しても、「どのタスクが遅いのか」「どのタスクがクラッシュを引き起こしたのか」を特定するのが難しく、Task を識別する手がかりはタスクのポインタ値やスタック情報しかなかったということです。

pthread には名前が、Grand Central Dispatch のキューにはラベルがあり、デバッグや計測のときに重要な文脈を与えてくれます。Swift Concurrencyでも同じように、開発者が自分のワークロードを説明する短い文字列を Task に添えられるようにしたい、というのが出発点です。

たとえば次のような Task があるとき、

let getUsers = Task {
    await users.get(accountID)
}

デバッガでこのタスクだけを他の多数のタスクから区別するには、開発者が独自に task-local を用意して「名前っぽいもの」を自力で伝えるしかなく、標準的な仕組みがないためツール側もそれを拾えませんでした。

02 どのように解決されるのか

Task を生成するAPIに、人間可読な名前を渡せる name: パラメータを追加します。名前は任意の String? で、実行時に文字列補間で組み立てることもできます。

let getUsers = Task(name: "Get Users") {
    await users.get(accountID)
}

// 実行時情報を含めたい場合
let getUsers = Task(name: "Get Users for \(accountID)") {
    await users.get(accountID)
}

名前を標準APIとして定義することで、デバッガやプロファイラ、swift-inspect のようなランタイム検査ツールが、タスクの名前を認識して「どの accountID のリクエストが遅いのか」などをそのまま表示できるようになります。

名前を付けられるAPI

名前はタスク生成時のみ指定でき、あとから変更することはできません。名前を受け取る形のイニシャライザ/メソッドが、非構造化タスクとタスクグループの双方に追加されます。

extension Task {
    init(
        name: String?,
        executorPreference taskExecutor: (any TaskExecutor)? = nil,
        priority: TaskPriority? = nil,
        operation: sending @escaping @isolated(any) () async -> Success
    )

    static func detached(
        name: String?,
        executorPreference taskExecutor: (any TaskExecutor)? = nil,
        priority: TaskPriority? = nil,
        operation: sending @escaping @isolated(any) () async -> Success
    )
}

Task.detached にも同様の形で name: が加わり、throwing 版・non-throwing 版の両方に対応します。

タスクグループの addTask / addTaskUnlessCancelled にも同じスタイルで name: が追加されます。throwing なグループや discarding なグループも含め、すべての種類のタスクグループで利用できます。

await withTaskGroup(of: Image.self) { group in
    group.addTask(name: "download profile image for \(userID)") {
        await downloadProfileImage(userID)
    }
    group.addTask(name: "download header image for \(userID)") {
        await downloadHeaderImage(userID)
    }
    // ...
}

名前の読み取り

付けた名前は、実行中にプログラムから読み出すこともできます。現在のタスクに対しては Task.name という静的プロパティ、特定の Task 参照や UnsafeCurrentTask に対してはインスタンスプロパティとして提供されます。

extension Task {
    static var name: String? { get }
    var name: String? { get }
}

extension UnsafeCurrentTask {
    var name: String? { get }
}

Taskname は、タスクが「完了」した後でも安全にアクセスできます。一方 UnsafeCurrentTask.name は、他のプロパティと同じく、基盤となるタスクが解放された後にアクセすると未定義動作になります(Unsafe 接頭辞どおりの扱いです)。

エグゼキュータからのタスク名参照

エグゼキュータがログに「いまどのタスクを動かしているか」を出したい、というニーズに応えるため、ExecutorJob / UnownedJob からその job が表すタスクを取り出す口も追加されます。

extension ExecutorJob {
    public var unsafeCurrentTask: UnsafeCurrentTask? { get }
}

これにより、たとえばカスタムエグゼキュータの enqueue(_:) で、

public nonisolated func enqueue(_ job: consuming ExecutorJob) {
    log.trace("Running task named: \(job.unsafeCurrentTask?.name ?? "<no-name>")")
    // ...
}

のように、ジョブに紐づくタスクの名前をログに出せます。ここで UnsafeCurrentTask を使うのは、ジョブに対して runSynchronously を呼んだ後は対象のタスクがすでに破棄されている可能性があり、生存期間を呼び出し側が把握している必要があるためです。将来 UnsafeCurrentTask に task local 値のアクセサなどが加わった際も、同じ経路でそれらを引き出せるようになります。

ランタイム要件

名前の保持にはランタイム側の変更が必要なため、これらのAPIは新しいOS上でのみ利用できます。名前はあくまでオプショナルなので、既存のコードへの互換性影響はありません。

Future Directions

async let で作られるタスクへの命名は、このProposalのスコープには含まれません。async let は右辺が直接 Task 初期化子を持つわけではなく、自然に文字列を渡せる構文上の場所がないためです。将来的には、囲みスコープの名前から getUserImages.asyncLet-1 のような既定名を自動生成する案や、TaskGroup に移行して明示的に命名する案が議論されており、より細かい制御が欲しい場合は現時点でも TaskGroup を使うのが回避策になります。ただし、どの形で導入されるかは未定です。