Skip to content

Commit 6cbb792

Browse files
committed
[TaskLocals] Propagate task-locals through async{}
1 parent 82e91b7 commit 6cbb792

File tree

9 files changed

+223
-20
lines changed

9 files changed

+223
-20
lines changed

include/swift/ABI/TaskLocal.h

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,8 @@ class TaskLocal {
118118
reinterpret_cast<char *>(this) + storageOffset(valueType));
119119
}
120120

121+
void copyTo(AsyncTask *task);
122+
121123
/// Compute the offset of the storage from the base of the item.
122124
static size_t storageOffset(const Metadata *valueType) {
123125
size_t offset = sizeof(Item);
@@ -189,6 +191,18 @@ class TaskLocal {
189191
/// can be safely disposed of.
190192
bool popValue(AsyncTask *task);
191193

194+
/// Copy all task-local bindings to the target task.
195+
///
196+
/// The new bindings allocate their own items and can out-live the current task.
197+
///
198+
/// ### Optimizations
199+
/// Only the most recent binding of a value is copied over, i.e. given
200+
/// a key bound to `A` and then `B`, only the `B` binding will be copied.
201+
/// This is safe and correct because the new task would never have a chance
202+
/// to observe the `A` value, because it semantically will never observe a
203+
/// "pop" of the `B` value - it was spawned from a scope where only B was observable.
204+
void copyTo(AsyncTask *target);
205+
192206
/// Destroy and deallocate all items stored by this specific task.
193207
///
194208
/// Items owned by a parent task are left untouched, since we do not own them.

include/swift/Runtime/Concurrency.h

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -480,6 +480,16 @@ void swift_task_localValuePush(const HeapObject *key,
480480
SWIFT_EXPORT_FROM(swift_Concurrency) SWIFT_CC(swift)
481481
void swift_task_localValuePop();
482482

483+
/// Copy all task locals from the current context to the target task.
484+
///
485+
/// Its Swift signature is
486+
///
487+
/// \code
488+
/// func _taskLocalValueGet<Key>(AsyncTask* task)
489+
/// \endcode
490+
SWIFT_EXPORT_FROM(swift_Concurrency) SWIFT_CC(swift)
491+
void swift_task_localsCopyTo(AsyncTask* target);
492+
483493
/// This should have the same representation as an enum like this:
484494
/// enum NearestTaskDeadline {
485495
/// case none

stdlib/public/CompatibilityOverride/CompatibilityOverrideConcurrency.def

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,12 @@ OVERRIDE_TASK_LOCAL(task_localValuePop, void,
247247
SWIFT_EXPORT_FROM(swift_Concurrency), SWIFT_CC(swift),
248248
swift::, ,)
249249

250+
OVERRIDE_TASK_LOCAL(task_localsCopyTo, void,
251+
SWIFT_EXPORT_FROM(swift_Concurrency), SWIFT_CC(swift),
252+
swift::,
253+
(AsyncTask *target),
254+
(target))
255+
250256
OVERRIDE_TASK_STATUS(task_addStatusRecord, bool,
251257
SWIFT_EXPORT_FROM(swift_Concurrency), SWIFT_CC(swift),
252258
swift::, (TaskStatusRecord *newRecord), (newRecord))

stdlib/public/Concurrency/Task.cpp

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,10 @@ AsyncTask::~AsyncTask() {
195195
}
196196

197197
// Release any objects potentially held as task local values.
198+
//
199+
// This must be called last when destroying a task - to keep stack discipline of the allocator.
200+
// because it may have created some task-locals immediately upon creation,
201+
// e.g. if the task is spawned with async{} and inherited some task-locals.
198202
Local.destroy(this);
199203
}
200204

stdlib/public/Concurrency/Task.swift

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -535,6 +535,15 @@ public func async<T>(
535535
// Create the asynchronous task future.
536536
let (task, _) = Builtin.createAsyncTaskFuture(flags.bits, operation)
537537

538+
// Copy all task locals to the newly created task.
539+
// We must copy them rather than point to the current task since the new task
540+
// is not structured and may out-live the current task.
541+
//
542+
// WARNING: This MUST be done BEFORE we enqueue the task,
543+
// because it acts as-if it was running inside the task and thus does not
544+
// take any extra steps to synchronize the task-local operations.
545+
_taskLocalsCopy(to: task)
546+
538547
// Enqueue the resulting job.
539548
_enqueueJobGlobal(Builtin.convertTaskToJob(task))
540549

stdlib/public/Concurrency/TaskLocal.cpp

Lines changed: 77 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
#include "swift/ABI/Metadata.h"
2525
#include "llvm/ADT/PointerIntPair.h"
2626
#include "TaskPrivate.h"
27+
#include <set>
2728

2829
#if defined(__APPLE__)
2930
#include <asl.h>
@@ -134,6 +135,22 @@ static void swift_task_localValuePopImpl() {
134135
assert(false && "Attempted to pop value but no task or thread-local storage available!");
135136
}
136137

138+
SWIFT_CC(swift)
139+
static void swift_task_localsCopyToImpl(AsyncTask *task) {
140+
TaskLocal::Storage *Local = nullptr;
141+
142+
if (AsyncTask *task = swift_task_getCurrent()) {
143+
Local = &task->Local;
144+
} else if (auto *storage = FallbackTaskLocalStorage::get()) {
145+
Local = storage;
146+
} else {
147+
// bail out, there are no values to copy
148+
return;
149+
}
150+
151+
Local->copyTo(task);
152+
}
153+
137154
// =============================================================================
138155
// ==== Initialization ---------------------------------------------------------
139156

@@ -185,6 +202,38 @@ TaskLocal::Item::createParentLink(AsyncTask *task, AsyncTask *parent) {
185202
return item;
186203
}
187204

205+
TaskLocal::Item*
206+
TaskLocal::Item::createLink(AsyncTask *task,
207+
const HeapObject *key,
208+
const Metadata *valueType) {
209+
size_t amountToAllocate = Item::itemSize(valueType);
210+
void *allocation = task ? _swift_task_alloc_specific(task, amountToAllocate)
211+
: malloc(amountToAllocate);
212+
Item *item = new (allocation) Item(key, valueType);
213+
214+
auto next = task ? task->Local.head : FallbackTaskLocalStorage::get()->head;
215+
item->next = reinterpret_cast<uintptr_t>(next) |
216+
static_cast<uintptr_t>(NextLinkType::IsNext);
217+
218+
return item;
219+
}
220+
221+
222+
void TaskLocal::Item::copyTo(AsyncTask *target) {
223+
assert(target && "TaskLocal item attempt to copy to null target task!");
224+
225+
auto item = Item::createLink(target, this->key, this->valueType);
226+
valueType->vw_initializeWithTake(item->getStoragePtr(), this->getStoragePtr());
227+
228+
/// A `copyTo` may ONLY be invoked BEFORE the task is actually scheduled,
229+
/// so right now we can safely copy the value into the task without additional
230+
/// synchronization.
231+
target->Local.head = item;
232+
}
233+
234+
// =============================================================================
235+
// ==== checks -----------------------------------------------------------------
236+
188237
SWIFT_CC(swift)
189238
static void swift_task_reportIllegalTaskLocalBindingWithinWithTaskGroupImpl(
190239
const unsigned char *file, uintptr_t fileLength,
@@ -257,22 +306,6 @@ static void swift_task_reportIllegalTaskLocalBindingWithinWithTaskGroupImpl(
257306
abort();
258307
}
259308

260-
TaskLocal::Item*
261-
TaskLocal::Item::createLink(AsyncTask *task,
262-
const HeapObject *key,
263-
const Metadata *valueType) {
264-
size_t amountToAllocate = Item::itemSize(valueType);
265-
void *allocation = task ? _swift_task_alloc_specific(task, amountToAllocate)
266-
: malloc(amountToAllocate);
267-
Item *item = new (allocation) Item(key, valueType);
268-
269-
auto next = task ? task->Local.head : FallbackTaskLocalStorage::get()->head;
270-
item->next = reinterpret_cast<uintptr_t>(next) |
271-
static_cast<uintptr_t>(NextLinkType::IsNext);
272-
273-
return item;
274-
}
275-
276309
// =============================================================================
277310
// ==== destroy ----------------------------------------------------------------
278311

@@ -311,7 +344,7 @@ void TaskLocal::Storage::destroy(AsyncTask *task) {
311344
}
312345

313346
// =============================================================================
314-
// ==== push / pop / get -------------------------------------------------------
347+
// ==== Task Local Storage: operations -----------------------------------------
315348

316349
void TaskLocal::Storage::pushValue(AsyncTask *task,
317350
const HeapObject *key,
@@ -350,5 +383,32 @@ OpaqueValue* TaskLocal::Storage::getValue(AsyncTask *task,
350383
return nullptr;
351384
}
352385

386+
387+
void TaskLocal::Storage::copyTo(AsyncTask *target) {
388+
assert(target && "task must not be null when copying values into it");
389+
assert(!(target->Local.head) &&
390+
"Task must not have any task-local values bound before copying into it");
391+
392+
AsyncTask *task = swift_task_getCurrent();
393+
394+
// Set of keys for which we already have copied to the new task.
395+
// We only ever need to copy the *first* encounter of any given key,
396+
// because it is the most "specific"/"recent" binding and any other binding
397+
// of a key does not matter for the target task as it will never be able to
398+
// observe it.
399+
std::set<const HeapObject*> copied = {};
400+
401+
auto item = head;
402+
while (item) {
403+
// we only have to copy an item if it is the most recent binding of a key.
404+
// i.e. if we've already seen an item for this key, we can skip it.
405+
if (copied.emplace(item->key).second) {
406+
item->copyTo(target);
407+
}
408+
409+
item = item->getNext();
410+
}
411+
}
412+
353413
#define OVERRIDE_TASK_LOCAL COMPATIBILITY_OVERRIDE
354414
#include COMPATIBILITY_OVERRIDE_INCLUDE_PATH

stdlib/public/Concurrency/TaskLocal.swift

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -212,21 +212,27 @@ public final class TaskLocal<Value>: UnsafeSendable, CustomStringConvertible {
212212

213213
@available(SwiftStdlib 5.5, *)
214214
@_silgen_name("swift_task_localValuePush")
215-
public func _taskLocalValuePush<Value>(
215+
func _taskLocalValuePush<Value>(
216216
key: Builtin.RawPointer/*: Key*/,
217217
value: __owned Value
218218
) // where Key: TaskLocal
219219

220220
@available(SwiftStdlib 5.5, *)
221221
@_silgen_name("swift_task_localValuePop")
222-
public func _taskLocalValuePop()
222+
func _taskLocalValuePop()
223223

224224
@available(SwiftStdlib 5.5, *)
225225
@_silgen_name("swift_task_localValueGet")
226-
public func _taskLocalValueGet(
226+
func _taskLocalValueGet(
227227
key: Builtin.RawPointer/*Key*/
228228
) -> UnsafeMutableRawPointer? // where Key: TaskLocal
229229

230+
@available(SwiftStdlib 5.5, *)
231+
@_silgen_name("swift_task_localsCopyTo")
232+
func _taskLocalsCopy(
233+
to target: Builtin.NativeObject
234+
)
235+
230236
// ==== Checks -----------------------------------------------------------------
231237

232238
@available(SwiftStdlib 5.5, *)
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
// RUN: %target-run-simple-swift(-Xfrontend -enable-experimental-concurrency -parse-as-library %import-libdispatch) | %FileCheck %s
2+
3+
// REQUIRES: executable_test
4+
// REQUIRES: concurrency
5+
// REQUIRES: libdispatch
6+
7+
// rdar://76038845
8+
// UNSUPPORTED: use_os_stdlib
9+
// UNSUPPORTED: back_deployment_runtime
10+
11+
@available(macOS 9999, iOS 9999, watchOS 9999, tvOS 9999, *)
12+
enum TL {
13+
@TaskLocal
14+
static var number: Int = 0
15+
@TaskLocal
16+
static var other: Int = 0
17+
}
18+
19+
@available(macOS 9999, iOS 9999, watchOS 9999, tvOS 9999, *)
20+
@discardableResult
21+
func printTaskLocal<V>(
22+
_ key: TaskLocal<V>,
23+
_ expected: V? = nil,
24+
file: String = #file, line: UInt = #line
25+
) -> V? {
26+
let value = key.get()
27+
print("\(key) (\(value)) at \(file):\(line)")
28+
if let expected = expected {
29+
assert("\(expected)" == "\(value)",
30+
"Expected [\(expected)] but found: \(value), at \(file):\(line)")
31+
}
32+
return expected
33+
}
34+
35+
// ==== ------------------------------------------------------------------------
36+
37+
@available(macOS 9999, iOS 9999, watchOS 9999, tvOS 9999, *)
38+
func copyTo_async() async {
39+
await TL.$number.withValue(1111) {
40+
printTaskLocal(TL.$number) // CHECK: TaskLocal<Int>(defaultValue: 0) (1111)
41+
42+
await TL.$number.withValue(2222) {
43+
await TL.$other.withValue(9999) {
44+
printTaskLocal(TL.$number) // CHECK: TaskLocal<Int>(defaultValue: 0) (2222)
45+
printTaskLocal(TL.$other) // CHECK: TaskLocal<Int>(defaultValue: 0) (9999)
46+
let handle = async {
47+
printTaskLocal(TL.$number) // CHECK: TaskLocal<Int>(defaultValue: 0) (2222)
48+
printTaskLocal(TL.$other) // CHECK: TaskLocal<Int>(defaultValue: 0) (9999)
49+
TL.$number.withValue(3333) {
50+
printTaskLocal(TL.$number) // CHECK: TaskLocal<Int>(defaultValue: 0) (3333)
51+
printTaskLocal(TL.$other) // CHECK: TaskLocal<Int>(defaultValue: 0) (9999)
52+
}
53+
}
54+
55+
_ = await handle.get()
56+
}
57+
}
58+
}
59+
}
60+
61+
@available(macOS 9999, iOS 9999, watchOS 9999, tvOS 9999, *)
62+
func copyTo_async_noWait() async {
63+
print(#function)
64+
TL.$number.withValue(1111) {
65+
TL.$number.withValue(2222) {
66+
TL.$other.withValue(9999) {
67+
async {
68+
printTaskLocal(TL.$number) // CHECK: TaskLocal<Int>(defaultValue: 0) (2222)
69+
printTaskLocal(TL.$other) // CHECK: TaskLocal<Int>(defaultValue: 0) (9999)
70+
TL.$number.withValue(3333) {
71+
printTaskLocal(TL.$number) // CHECK: TaskLocal<Int>(defaultValue: 0) (3333)
72+
printTaskLocal(TL.$other) // CHECK: TaskLocal<Int>(defaultValue: 0) (9999)
73+
}
74+
}
75+
}
76+
}
77+
}
78+
79+
let second = UInt64(100_000_000) // ns
80+
await Task.sleep(2 * second)
81+
}
82+
83+
@available(macOS 9999, iOS 9999, watchOS 9999, tvOS 9999, *)
84+
@main struct Main {
85+
static func main() async {
86+
await copyTo_async()
87+
await copyTo_async()
88+
await copyTo_async_noWait()
89+
}
90+
}

unittests/runtime/CompatibilityOverrideConcurrency.cpp

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,10 @@ TEST_F(CompatibilityOverrideConcurrencyTest, test_swift_task_localValuePop) {
236236
swift_task_localValuePop();
237237
}
238238

239+
TEST_F(CompatibilityOverrideConcurrencyTest, test_swift_task_localsCopyTo) {
240+
swift_task_localsCopyTo(nullptr);
241+
}
242+
239243
TEST_F(CompatibilityOverrideConcurrencyTest, test_swift_task_addStatusRecord) {
240244
swift_task_addStatusRecord(nullptr);
241245
}

0 commit comments

Comments
 (0)