Skip to content

Commit 070f9a7

Browse files
committed
[TaskLocals] review 2: projected value wrapper
1 parent 347d540 commit 070f9a7

8 files changed

+259
-178
lines changed

stdlib/public/Concurrency/TaskLocal.swift

Lines changed: 140 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -13,75 +13,157 @@
1313
import Swift
1414
@_implementationOnly import _SwiftConcurrencyShims
1515

16+
/// Property wrapper that defines a task-local value key.
17+
///
18+
/// A task-local value is a value that can be bound and read in the context of a
19+
/// `Task`. It is implicitly carried with the task, and is accessible by any
20+
/// child tasks the task creates (such as TaskGroup or `async let` created tasks).
21+
///
22+
/// ### Task-local declarations
23+
///
24+
/// Task locals must be declared as static properties (or global properties,
25+
/// once property wrappers support these), like this:
26+
///
27+
/// enum TracingExample {
28+
/// @TaskLocal
29+
/// static let traceID: TraceID?
30+
/// }
31+
///
32+
/// ### Default values
33+
/// Task local values of optional types default to `nil`. It is possible to define
34+
/// not-optional task-local values, and an explicit default value must then be
35+
/// defined instead.
36+
///
37+
/// The default value is returned whenever the task-local is read
38+
/// from a context which either: has no task available to read the value from
39+
/// (e.g. a synchronous function, called without any asynchronous function in its call stack),
40+
///
41+
///
42+
/// ### Reading task-local values
43+
/// Reading task local values is simple and looks the same as-if reading a normal
44+
/// static property:
45+
///
46+
/// guard let traceID = TracingExample.traceID else {
47+
/// print("no trace id")
48+
/// return
49+
/// }
50+
/// print(traceID)
51+
///
52+
/// It is possible to perform task-local value reads from either asynchronous
53+
/// or synchronous functions. Within asynchronous functions, as a "current" task
54+
/// is always guaranteed to exist, this will perform the lookup in the task local context.
55+
///
56+
/// A lookup made from the context of a synchronous function, that is not called
57+
/// from an asynchronous function (!), will immediately return the task-local's
58+
/// default value.
59+
///
60+
/// ### Binding task-local values
61+
/// Task local values cannot be `set` directly and must instead be bound using
62+
/// the scoped `$traceID.withValue() { ... }` operation. The value is only bound
63+
/// for the duration of that scope, and is available to any child tasks which
64+
/// are created within that scope.
65+
///
66+
/// Detached tasks do not inherit task-local values, however tasks created using
67+
/// the `async {}` operation do inherit task-locals by copying them to the new
68+
/// asynchronous task, even though it is an un-structured task.
69+
///
70+
/// ### Examples
71+
///
72+
/// @TaskLocal
73+
/// static var traceID: TraceID?
74+
///
75+
/// print("traceID: \(traceID)") // traceID: nil
76+
///
77+
/// $traceID.withValue(1234) { // bind the value
78+
/// print("traceID: \(traceID)") // traceID: 1234
79+
/// call() // traceID: 1234
80+
///
81+
/// asyncDetached { // detached tasks do not inherit task-local values
82+
/// call() // traceID: nil
83+
/// }
84+
///
85+
/// async { // async tasks do inherit task locals by copying
86+
/// call() // traceID: 1234
87+
/// }
88+
/// }
89+
///
90+
///
91+
/// func call() {
92+
/// print("traceID: \(traceID)") // 1234
93+
/// }
94+
///
1695
/// This type must be a `class` so it has a stable identity, that is used as key
1796
/// value for lookups in the task local storage.
1897
@propertyWrapper
1998
@available(macOS 9999, iOS 9999, watchOS 9999, tvOS 9999, *)
20-
public final class TaskLocal<Value: Sendable>: CustomStringConvertible {
21-
// only reason this is ! is to store the wrapper `self` in Access so we
22-
// can use its identity as the key for lookups.
23-
private var access: Access!
99+
public final class TaskLocal<Value: Sendable>: Sendable, CustomStringConvertible {
100+
let defaultValue: Value
24101

25-
public init(default defaultValue: Value) {
26-
self.access = Access(key: self, defaultValue: defaultValue)
102+
public init(wrappedValue defaultValue: Value) {
103+
self.defaultValue = defaultValue
27104
}
28105

106+
var key: Builtin.RawPointer {
107+
unsafeBitCast(self, to: Builtin.RawPointer.self)
108+
}
29109

30-
public struct Access: CustomStringConvertible {
31-
let key: Builtin.RawPointer
32-
let defaultValue: Value
33-
34-
init(key: TaskLocal<Value>, defaultValue: Value) {
35-
self.key = unsafeBitCast(key, to: Builtin.RawPointer.self)
36-
self.defaultValue = defaultValue
37-
}
38-
39-
public func get() -> Value {
40-
withUnsafeCurrentTask { task in
41-
guard let task = task else {
42-
return self.defaultValue
43-
}
44-
45-
let value = _taskLocalValueGet(task._task, key: key)
110+
/// Gets the value currently bound to this task-local from the current task.
111+
///
112+
/// If no current task is available in the context where this call is made,
113+
/// or if the task-local has no value bound, this will return the `defaultValue`
114+
/// of the task local.
115+
public func get() -> Value {
116+
withUnsafeCurrentTask { task in
117+
guard let task = task else {
118+
return self.defaultValue
119+
}
46120

47-
guard let rawValue = value else {
48-
return self.defaultValue
49-
}
121+
let value = _taskLocalValueGet(task._task, key: key)
50122

51-
// Take the value; The type should be correct by construction
52-
let storagePtr =
53-
rawValue.bindMemory(to: Value.self, capacity: 1)
54-
return UnsafeMutablePointer<Value>(mutating: storagePtr).pointee
123+
guard let rawValue = value else {
124+
return self.defaultValue
55125
}
56-
}
57126

58-
/// Execute the `body` closure
59-
@discardableResult
60-
public func withValue<R>(_ valueDuringBody: Value, do body: () async throws -> R,
61-
file: String = #file, line: UInt = #line) async rethrows -> R {
62-
// check if we're not trying to bind a value from an illegal context; this may crash
63-
_checkIllegalTaskLocalBindingWithinWithTaskGroup(file: file, line: line)
64-
65-
// we need to escape the `_task` since the withUnsafeCurrentTask closure is not `async`.
66-
// this is safe, since we know the task will remain alive because we are running inside of it.
67-
let _task = withUnsafeCurrentTask { task in
68-
task!._task // !-safe, guaranteed to have task available inside async function
69-
}
127+
// Take the value; The type should be correct by construction
128+
let storagePtr =
129+
rawValue.bindMemory(to: Value.self, capacity: 1)
130+
return UnsafeMutablePointer<Value>(mutating: storagePtr).pointee
131+
}
132+
}
70133

71-
_taskLocalValuePush(_task, key: key, value: valueDuringBody)
72-
defer { _taskLocalValuePop(_task) }
134+
/// Binds the task-local to the specific value for the duration of the body.
135+
///
136+
/// The value is available throughout the execution of the body closure,
137+
/// including any `get` operations performed by child-tasks created during the
138+
/// execution of the body closure.
139+
///
140+
/// If the same task-local is bound multiple times, be it in the same task, or
141+
/// in specific child tasks, the more specific (i.e. "deeper") binding is
142+
/// returned when the value is read.
143+
///
144+
/// If the value is a reference type, it will be retained for the duration of
145+
/// the body closure.
146+
@discardableResult
147+
public func withValue<R>(_ valueDuringBody: Value, do body: () async throws -> R,
148+
file: String = #file, line: UInt = #line) async rethrows -> R {
149+
// check if we're not trying to bind a value from an illegal context; this may crash
150+
_checkIllegalTaskLocalBindingWithinWithTaskGroup(file: file, line: line)
73151

74-
return try await body()
152+
// we need to escape the `_task` since the withUnsafeCurrentTask closure is not `async`.
153+
// this is safe, since we know the task will remain alive because we are running inside of it.
154+
let _task = withUnsafeCurrentTask { task in
155+
task!._task // !-safe, guaranteed to have task available inside async function
75156
}
76157

77-
public var description: String {
78-
"TaskLocal<\(Value.self)>.Access"
79-
}
158+
_taskLocalValuePush(_task, key: key, value: valueDuringBody)
159+
defer { _taskLocalValuePop(_task) }
160+
161+
return try await body()
80162
}
81163

82-
public var wrappedValue: TaskLocal<Value>.Access {
164+
public var projectedValue: TaskLocal<Value> {
83165
get {
84-
self.access
166+
self
85167
}
86168

87169
@available(*, unavailable, message: "use 'myTaskLocal.withValue(_:do:)' instead")
@@ -90,17 +172,14 @@ public final class TaskLocal<Value: Sendable>: CustomStringConvertible {
90172
}
91173
}
92174

93-
public var description: String {
94-
"\(Self.self)(defaultValue: \(self.access.defaultValue))"
175+
public var wrappedValue: Value {
176+
self.get()
95177
}
96178

97-
}
98-
99-
@available(macOS 9999, iOS 9999, watchOS 9999, tvOS 9999, *)
100-
extension TaskLocal {
101-
public convenience init<V>() where Value == Optional<V> {
102-
self.init(default: nil)
179+
public var description: String {
180+
"\(Self.self)(defaultValue: \(self.defaultValue))"
103181
}
182+
104183
}
105184

106185
@available(macOS 9999, iOS 9999, watchOS 9999, tvOS 9999, *)
@@ -113,13 +192,14 @@ extension UnsafeCurrentTask {
113192
/// represented by this object.
114193
@discardableResult
115194
public func withTaskLocal<Value: Sendable, R>(
116-
_ access: TaskLocal<Value>.Access, boundTo valueDuringBody: Value,
195+
_ taskLocal: TaskLocal<Value>,
196+
boundTo valueDuringBody: Value,
117197
do body: () throws -> R,
118198
file: String = #file, line: UInt = #line) rethrows -> R {
119199
// check if we're not trying to bind a value from an illegal context; this may crash
120200
_checkIllegalTaskLocalBindingWithinWithTaskGroup(file: file, line: line)
121201

122-
_taskLocalValuePush(self._task, key: access.key, value: valueDuringBody)
202+
_taskLocalValuePush(self._task, key: taskLocal.key, value: valueDuringBody)
123203
defer { _taskLocalValuePop(_task) }
124204

125205
return try body()

0 commit comments

Comments
 (0)