Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions benchmark/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,7 @@ set(SWIFT_BENCH_MODULES
single-source/Suffix
single-source/SuperChars
single-source/TaskGroups
single-source/TaskID
single-source/TaskLocalGet
single-source/ToddCoxeter
single-source/TwoSum
Expand Down
92 changes: 92 additions & 0 deletions benchmark/single-source/TaskID.swift
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
2 changes: 2 additions & 0 deletions benchmark/utils/main.swift
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,7 @@ import SubstringTest
import Suffix
import SuperChars
import TaskGroups
import TaskID
import TaskLocalGet
import ToddCoxeter
import TwoSum
Expand Down Expand Up @@ -433,6 +434,7 @@ register(SubstringTest.benchmarks)
register(Suffix.benchmarks)
register(SuperChars.benchmarks)
register(TaskGroups.benchmarks)
register(TaskID.benchmarks)
register(TaskLocalGet.benchmarks)
register(ToddCoxeter.benchmarks)
register(TwoSum.benchmarks)
Expand Down
8 changes: 8 additions & 0 deletions include/swift/Runtime/Concurrency.h
Original file line number Diff line number Diff line change
Expand Up @@ -858,6 +858,14 @@ bool swift_executor_isComplexEquality(SerialExecutorRef ref);
SWIFT_EXPORT_FROM(swift_Concurrency) SWIFT_CC(swift)
uint64_t swift_task_getJobTaskId(Job *job);

/// Return the 64bit TaskID of the currently-executing AsyncTask,
/// or 0 if there is no current task. This is more efficient than getting
/// the task object to Swift and then getting the ID from it.
///
/// Available: Swift 6.5
SWIFT_EXPORT_FROM(swift_Concurrency) SWIFT_CC(swift)
uint64_t swift_task_getCurrentTaskId(void);
Copy link
Copy Markdown
Contributor Author

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_getJobTaskId on it; because getting it into swift caused an ARC operation on the task handle.


#if SWIFT_CONCURRENCY_ENABLE_DISPATCH

/// Enqueue the given job on the main executor.
Expand Down
8 changes: 8 additions & 0 deletions stdlib/public/Concurrency/Executor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -941,9 +941,17 @@ func _checkExpectedExecutor(_filenameStart: Builtin.RawPointer,
///
/// - Returns: the Id stored in this ExecutorJob or Task, for purposes of debug printing
@available(StdlibDeploymentTarget 5.9, *)
@usableFromInline
@_silgen_name("swift_task_getJobTaskId")
internal func _getJobTaskId(_ job: UnownedJob) -> UInt64

/// Returns the 64-bit TaskID of the currently-executing task, or 0 if there
/// is no current task.
@available(StdlibDeploymentTarget 6.5, *)
@usableFromInline
@_silgen_name("swift_task_getCurrentTaskId")
internal func _getCurrentTaskId() -> UInt64

@available(SwiftStdlib 5.9, *)
@_silgen_name("_task_serialExecutor_isSameExclusiveExecutionContext")
internal func _task_serialExecutor_isSameExclusiveExecutionContext<E>(current currentExecutor: E, executor: E) -> Bool
Expand Down
8 changes: 8 additions & 0 deletions stdlib/public/Concurrency/GlobalExecutor.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,14 @@ uint64_t swift::swift_task_getJobTaskId(Job *job) {
}
}

uint64_t swift::swift_task_getCurrentTaskId() {
if (auto task = swift_task_getCurrent()) {
return task->getTaskId();
}
// 0 is never a valid task ID (see AsyncTask::setTaskId).
return 0;
}

extern "C" void *swift_job_alloc(SwiftJob *job, size_t size) {
auto task = cast<AsyncTask>(reinterpret_cast<Job *>(job));
return _swift_task_alloc_specific(task, size);
Expand Down
104 changes: 101 additions & 3 deletions stdlib/public/Concurrency/Task.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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 {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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.
Expand Down Expand Up @@ -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()
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The 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+

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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, *)
Expand Down Expand Up @@ -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 -----------------------------------------------------
Expand Down Expand Up @@ -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))
Expand Down Expand Up @@ -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, *)
Expand Down
49 changes: 49 additions & 0 deletions test/Concurrency/Runtime/async_task_id.swift
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
}
}
22 changes: 22 additions & 0 deletions test/abi/macOS/arm64/concurrency.swift
Original file line number Diff line number Diff line change
Expand Up @@ -437,3 +437,25 @@ Added: _$ss12ContinuationVMn
Added: _$ss12ContinuationVsRi_zrlE7contextBcvg
Added: _$ss12ContinuationVsRi_zrlEfD
Added: _$ss12ContinuationVsRi_zrlEyAByxq_GBccfC

// Task.id / UnsafeCurrentTask.id / Task.currentID
Added: _swift_task_getCurrentTaskId
Added: _$sScT2ids6TaskIDVvpMV
Added: _$sSct2ids6TaskIDVvpMV
Added: _$sScTss5NeverORszABRs_rlE9currentIDs04TaskC0VSgvpZMV

// TaskID struct (Sendable, Hashable)
Added: _$ss6TaskIDVMa
Added: _$ss6TaskIDVMn
Added: _$ss6TaskIDVN
Added: _$ss6TaskIDVSHsMc
Added: _$ss6TaskIDVSQsMc
Added: _$ss6TaskIDV2eeoiySbAB_ABtFZ
Added: _$ss6TaskIDV4hash4intoys6HasherVz_tF
Added: _$ss6TaskIDV8rawValues6UInt64VvpMV
Added: _$ss6TaskIDV9hashValueSivg
Added: _$ss6TaskIDV9hashValueSivpMV
Added: _$ss6TaskIDV9_rawValues6UInt64Vvg
Added: _$ss6TaskIDV9_rawValues6UInt64VvM
Added: _$ss6TaskIDV9_rawValues6UInt64VvpMV
Added: _$ss6TaskIDV9_rawValues6UInt64Vvs
Loading