-
Notifications
You must be signed in to change notification settings - Fork 10.7k
[Concurrency] Introduce Task.id #89237
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,92 @@ | ||
| //===--- TaskID.swift -----------------------------------------------------===// | ||
| // | ||
| // This source file is part of the Swift.org open source project | ||
| // | ||
| // Copyright (c) 2026 Apple Inc. and the Swift project authors | ||
| // Licensed under Apache License v2.0 with Runtime Library Exception | ||
| // | ||
| // See https://swift.org/LICENSE.txt for license information | ||
| // See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors | ||
| // | ||
| //===----------------------------------------------------------------------===// | ||
|
|
||
| // Measures the cost of reading the current Task's ID. | ||
|
|
||
| import TestsUtils | ||
|
|
||
| public var benchmarks: [BenchmarkInfo] { | ||
| guard #available(macOS 9999, iOS 9999, tvOS 9999, watchOS 9999, *) else { | ||
| return [] | ||
| } | ||
| return [ | ||
| BenchmarkInfo( | ||
| name: "TaskID.currentID", | ||
| runFunction: run_TaskID_currentID, | ||
| tags: [.concurrency] | ||
| ), | ||
| BenchmarkInfo( | ||
| name: "TaskID.withUnsafeCurrentTask.id", | ||
| runFunction: run_TaskID_withUnsafeCurrentTask, | ||
| tags: [.concurrency] | ||
| ), | ||
| BenchmarkInfo( | ||
| name: "TaskID.directBuiltins", | ||
| runFunction: run_TaskID_directBuiltins, | ||
| tags: [.concurrency] | ||
| ), | ||
| ] | ||
| } | ||
|
|
||
| // Direct accessor: single runtime call, no closure, no ARC on the task. | ||
| @available(macOS 9999, iOS 9999, tvOS 9999, watchOS 9999, *) | ||
| private func run_TaskID_currentID(_ n: Int) async { | ||
| for _ in 0..<n { | ||
| for _ in 0..<10_000 { | ||
| blackHole(Task.currentID) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| // Equivalent shape built on withUnsafeCurrentTask. | ||
| @available(macOS 9999, iOS 9999, tvOS 9999, watchOS 9999, *) | ||
| private func run_TaskID_withUnsafeCurrentTask(_ n: Int) async { | ||
| for _ in 0..<n { | ||
| for _ in 0..<10_000 { | ||
| withUnsafeCurrentTask { task in | ||
| blackHole(task?.id) | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| #if canImport(Darwin) | ||
| import Darwin | ||
|
|
||
| private typealias _GetCurrentTaskFn = @convention(c) () -> OpaquePointer? | ||
| private typealias _GetJobTaskIdFn = @convention(c) (OpaquePointer) -> UInt64 | ||
|
|
||
| private let _bench_swift_task_getCurrent: _GetCurrentTaskFn = unsafeBitCast( | ||
| dlsym(UnsafeMutableRawPointer(bitPattern: -2), "swift_task_getCurrent")!, | ||
| to: _GetCurrentTaskFn.self | ||
| ) | ||
| private let _bench_swift_task_getJobTaskId: _GetJobTaskIdFn = unsafeBitCast( | ||
| dlsym(UnsafeMutableRawPointer(bitPattern: -2), "swift_task_getJobTaskId")!, | ||
| to: _GetJobTaskIdFn.self | ||
| ) | ||
|
|
||
| @available(macOS 9999, iOS 9999, tvOS 9999, watchOS 9999, *) | ||
| private func run_TaskID_directBuiltins(_ n: Int) async { | ||
| for _ in 0..<n { | ||
| for _ in 0..<10_000 { | ||
| if let task = _bench_swift_task_getCurrent() { | ||
| blackHole(_bench_swift_task_getJobTaskId(task)) | ||
| } else { | ||
| blackHole(UInt64(0)) | ||
| } | ||
| } | ||
| } | ||
| } | ||
| #else | ||
| @available(macOS 9999, iOS 9999, tvOS 9999, watchOS 9999, *) | ||
| private func run_TaskID_directBuiltins(_ n: Int) async {} | ||
| #endif |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -290,6 +290,49 @@ extension Task: Equatable { | |
| } | ||
| } | ||
|
|
||
| // ==== ----------------------------------------------------------------------- | ||
| // MARK: Task ID | ||
|
|
||
| /// An opaque, process-unique identifier for a Swift ``Task``. | ||
| /// | ||
| /// A `TaskID` is assigned at task creation, never changes for the lifetime | ||
| /// of the task, and is never reused once a task has completed. IDs are | ||
| /// scoped to the current process and are not suitable for cross-process | ||
| /// correlation. | ||
| /// | ||
| /// Reading the ID of the currently-executing task is fast. | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think we should expand this part. I will come up with a suggestion if you haven't already. |
||
| /// | ||
| /// - SeeAlso: ``Task/currentID`` | ||
| /// - SeeAlso: ``Task/id`` | ||
| /// - SeeAlso: ``UnsafeCurrentTask/id`` | ||
| @available(StdlibDeploymentTarget 6.5, *) | ||
| @frozen | ||
| public struct TaskID: Sendable, Hashable { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sorry for asking, but why do we want to have this as a separate type? Isn't this just a single UInt64? Not that I'm against using newtypes. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You can see rationale in the draft proposal:
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks for the info! I agree using a newtype is more idiomatic. |
||
| @usableFromInline | ||
| internal var _rawValue: UInt64 | ||
|
|
||
| @_alwaysEmitIntoClient | ||
| internal init(_rawValue: UInt64) { | ||
| self._rawValue = _rawValue | ||
| } | ||
|
|
||
| /// The raw 64-bit value of this ID. | ||
| /// | ||
| /// This is the only escape hatch from the opaque type and is intended for | ||
| /// serialization, logging, or interop with tools that expect a numeric | ||
| /// task ID. The numeric value carries no semantic meaning beyond the | ||
| /// guarantees on `TaskID` itself. | ||
| @_alwaysEmitIntoClient | ||
| public var rawValue: UInt64 { _rawValue } | ||
| } | ||
|
|
||
| @available(StdlibDeploymentTarget 6.5, *) | ||
| extension Task { | ||
| /// A type alias for ``TaskID``, providing the spelling | ||
| /// `Task.ID` at the use site. | ||
| public typealias ID = TaskID | ||
| } | ||
|
|
||
| // ==== Task Priority ---------------------------------------------------------- | ||
|
|
||
| /// The priority of a task. | ||
|
|
@@ -439,6 +482,25 @@ extension Task where Success == Never, Failure == Never { | |
| } | ||
| } | ||
|
|
||
| /// The stable ID of the currently-executing task. | ||
| /// | ||
| /// If you access this static property outside the execution context of a | ||
| /// task, it will return `nil`. | ||
| /// | ||
| /// This ID is quick to obtain and can be used to reliably identify a | ||
| /// task, instead of its memory address which may be reused once the task | ||
| /// has been destroyed. No guarantees are made about its exact numeric | ||
| /// value. | ||
| /// | ||
| /// - SeeAlso: ``Task/id`` | ||
| /// - SeeAlso: ``UnsafeCurrentTask/id`` | ||
| @available(StdlibDeploymentTarget 6.5, *) | ||
| @_alwaysEmitIntoClient | ||
| public static var currentID: Task.ID? { | ||
| let id = _getCurrentTaskId() | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Backdeployment note: We could backdeploy this more if we do the "get task -> get id from job" and use the more efficient version in 6.5+
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think that would be a desirable idea. |
||
| return id == 0 ? nil : TaskID(_rawValue: id) | ||
| } | ||
|
|
||
| } | ||
|
|
||
| @available(SwiftStdlib 5.1, *) | ||
|
|
@@ -643,6 +705,26 @@ extension Task { | |
| nil | ||
| } | ||
| } | ||
|
|
||
| /// A stable ID for this task. | ||
| /// | ||
| /// This ID is quick to obtain and can be used to reliably identify a | ||
| /// task, instead of its memory address which may be reused once the task | ||
| /// has been destroyed. No guarantees are made about its exact numeric | ||
| /// value. | ||
| /// | ||
| /// The same ID is visible in tools such as `swift-inspect` and | ||
| /// Instruments, which makes it a convenient correlation key for tracing | ||
| /// and logging. | ||
| /// | ||
| /// - SeeAlso: ``Task/currentID`` | ||
| /// - SeeAlso: ``UnsafeCurrentTask/id`` | ||
| @available(StdlibDeploymentTarget 6.5, *) | ||
| @_alwaysEmitIntoClient | ||
| public var id: ID { | ||
| unsafe .init( | ||
| _rawValue: _getJobTaskId(unsafeBitCast(_task, to: UnownedJob.self))) | ||
| } | ||
| } | ||
|
|
||
| // ==== Voluntary Suspension ----------------------------------------------------- | ||
|
|
@@ -719,9 +801,7 @@ public func withUnsafeCurrentTask<T>(body: (UnsafeCurrentTask?) throws -> T) ret | |
| return try body(nil) | ||
| } | ||
|
|
||
| // FIXME: This retain seems pretty wrong, however if we don't we WILL crash | ||
| // with "destroying a task that never completed" in the task's destroy. | ||
| // How do we solve this properly? | ||
| // This retain is here to counter the release that will happen when the task leaves scope. | ||
| Builtin.retain(_task) | ||
|
|
||
| return try unsafe body(UnsafeCurrentTask(_task)) | ||
|
|
@@ -902,6 +982,24 @@ public struct UnsafeCurrentTask { | |
| nil | ||
| } | ||
| } | ||
|
|
||
| /// A stable ID for the current task. | ||
| /// | ||
| /// This ID is quick to obtain and can be used to reliably identify a | ||
| /// task, instead of its memory address which may be reused once the task | ||
| /// has been destroyed. No guarantees are made about its exact numeric | ||
| /// value. | ||
| /// | ||
| /// Returns the same value as ``Task/id`` read on the owning task. | ||
| /// | ||
| /// - SeeAlso: ``Task/id`` | ||
| /// - SeeAlso: ``Task/currentID`` | ||
| @available(StdlibDeploymentTarget 6.5, *) | ||
| @_alwaysEmitIntoClient | ||
| public var id: Task.ID { | ||
| unsafe TaskID( | ||
| _rawValue: _getJobTaskId(unsafeBitCast(_task, to: UnownedJob.self))) | ||
| } | ||
| } | ||
|
|
||
| @available(SwiftStdlib 5.1, *) | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,49 @@ | ||
| // RUN: %target-run-simple-swift( -parse-as-library) | %FileCheck %s | ||
|
|
||
| // REQUIRES: executable_test | ||
| // REQUIRES: concurrency | ||
| // REQUIRES: concurrency_runtime | ||
| // UNSUPPORTED: back_deployment_runtime | ||
| // UNSUPPORTED: use_os_stdlib | ||
|
|
||
| @available(SwiftStdlib 6.5, *) | ||
| @main struct Main { | ||
| static func main() async { | ||
| // Task.currentID is non-nil inside a task | ||
| let current = Task.currentID | ||
| print("current id is some: \(current != nil)") | ||
| // CHECK: current id is some: true | ||
|
|
||
| // id is stable: reading twice yields the same value | ||
| let first = Task.currentID | ||
| let second = Task.currentID | ||
| print("stable: \(first == second)") | ||
| // CHECK: stable: true | ||
|
|
||
| // UnsafeCurrentTask.id agrees with Task.currentID | ||
| let matchesUnsafe: Bool = withUnsafeCurrentTask { task in | ||
| guard let task else { return false } | ||
| return task.id == Task.currentID | ||
| } | ||
| print("unsafe matches: \(matchesUnsafe)") | ||
| // CHECK: unsafe matches: true | ||
|
|
||
| // Task.ID exposes the raw 64-bit value | ||
| let raw = Task.currentID?.rawValue ?? 0 | ||
| print("raw is non-zero: \(raw != 0)") | ||
| // CHECK: raw is non-zero: true | ||
|
|
||
| // Different child tasks have different IDs | ||
| let t1 = Task { Task.currentID } | ||
| let t2 = Task { Task.currentID } | ||
| let id1 = await t1.value | ||
| let id2 = await t2.value | ||
| print("both non-nil: \(id1 != nil && id2 != nil)") | ||
| // CHECK: both non-nil: true | ||
| print("different: \(id1 != id2)") | ||
| // CHECK: different: true | ||
|
|
||
| print("done") | ||
| // CHECK: done | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is more efficient than getting the task builtin into swift and then call
swift_task_getJobTaskIdon it; because getting it into swift caused an ARC operation on the task handle.