Skip to content

Commit d138ca4

Browse files
authored
Merge pull request swiftlang#74038 from ktoso/pick-wip-task-locals-defensive-copying-rather-than-crashing
[6.0][Concurrency] Defensive copying of tasks locals in TaskGroups (rather than crashing)
2 parents 27e2ed1 + 2686ed4 commit d138ca4

12 files changed

+479
-147
lines changed

include/swift/ABI/TaskLocal.h

Lines changed: 57 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,14 @@ class TaskLocal {
4444
/// lookups by skipping empty parent tasks during get(), and explained
4545
/// in depth in `createParentLink`.
4646
IsParent = 0b01,
47+
/// The task local binding was created inside the body of a `withTaskGroup`,
48+
/// and therefore must either copy it, or crash when a child task is created
49+
/// using 'group.addTask' and it would refer to this task local.
50+
///
51+
/// Items of this kind must be copied by a group child task for access
52+
/// safety reasons, as otherwise the pop would happen before the child task
53+
/// has completed.
54+
IsNextCreatedInTaskGroupBody = 0b10,
4755
};
4856

4957
class Item {
@@ -101,20 +109,55 @@ class TaskLocal {
101109
/// the Item linked list into the appropriate parent.
102110
static Item *createParentLink(AsyncTask *task, AsyncTask *parent);
103111

112+
static Item *createLink(AsyncTask *task,
113+
const HeapObject *key,
114+
const Metadata *valueType,
115+
bool inTaskGroupBody);
116+
104117
static Item *createLink(AsyncTask *task,
105118
const HeapObject *key,
106119
const Metadata *valueType);
107120

121+
static Item *createLinkInTaskGroup(
122+
AsyncTask *task,
123+
const HeapObject *key,
124+
const Metadata *valueType);
125+
108126
void destroy(AsyncTask *task);
109127

110128
Item *getNext() {
111129
return reinterpret_cast<Item *>(next & ~statusMask);
112130
}
113131

132+
void relinkTaskGroupLocalHeadToSafeNext(Item* nextOverride) {
133+
assert(!getNext() &&
134+
"Can only relink task local item that was not pointing at anything yet");
135+
assert((nextOverride->isNextLinkPointer() ||
136+
nextOverride->isParentPointer()) &&
137+
"Currently relinking is only done within a task group to "
138+
"avoid within-taskgroup next pointers; attempted to point at "
139+
"task local declared within task group body though!");
140+
141+
next = reinterpret_cast<uintptr_t>(nextOverride) |
142+
static_cast<uintptr_t>((nextOverride->isNextLinkPointer()
143+
? NextLinkType::IsNextCreatedInTaskGroupBody
144+
: NextLinkType::IsParent));
145+
}
146+
114147
NextLinkType getNextLinkType() const {
115148
return static_cast<NextLinkType>(next & statusMask);
116149
}
117150

151+
bool isNextLinkPointer() const {
152+
return static_cast<NextLinkType>(next & statusMask) ==
153+
NextLinkType::IsNext;
154+
}
155+
156+
bool isNextLinkPointerCreatedInTaskGroupBody() const {
157+
return static_cast<NextLinkType>(next & statusMask) ==
158+
NextLinkType::IsNextCreatedInTaskGroupBody;
159+
}
160+
118161
/// Item does not contain any actual value, and is only used to point at
119162
/// a specific parent item.
120163
bool isEmpty() const {
@@ -127,7 +170,7 @@ class TaskLocal {
127170
reinterpret_cast<char *>(this) + storageOffset(valueType));
128171
}
129172

130-
void copyTo(AsyncTask *task);
173+
TaskLocal::Item* copyTo(AsyncTask *task);
131174

132175
/// Compute the offset of the storage from the base of the item.
133176
static size_t storageOffset(const Metadata *valueType) {
@@ -136,9 +179,9 @@ class TaskLocal {
136179
if (valueType) {
137180
size_t alignment = valueType->vw_alignment();
138181
return (offset + alignment - 1) & ~(alignment - 1);
139-
} else {
140-
return offset;
141182
}
183+
184+
return offset;
142185
}
143186

144187
/// Determine the size of the item given a particular value type.
@@ -187,6 +230,10 @@ class TaskLocal {
187230

188231
public:
189232

233+
/// Get the "current" task local storage from either the passed in
234+
/// task, or fall back to the *thread* local stored storage.
235+
static Storage* getCurrent(AsyncTask *task);
236+
190237
void initializeLinkParent(AsyncTask *task, AsyncTask *parent);
191238

192239
void pushValue(AsyncTask *task,
@@ -200,6 +247,9 @@ class TaskLocal {
200247
/// can be safely disposed of.
201248
bool popValue(AsyncTask *task);
202249

250+
/// Peek at the head item and get its type.
251+
std::optional<NextLinkType> peekHeadLinkType() const;
252+
203253
/// Copy all task-local bindings to the target task.
204254
///
205255
/// The new bindings allocate their own items and can out-live the current task.
@@ -212,6 +262,10 @@ class TaskLocal {
212262
/// "pop" of the `B` value - it was spawned from a scope where only B was observable.
213263
void copyTo(AsyncTask *target);
214264

265+
// FIXME(concurrency): We currently copy from "all" task groups we encounter
266+
// however in practice we only
267+
void copyToOnlyOnlyFromCurrentGroup(AsyncTask *target);
268+
215269
/// Destroy and deallocate all items stored by this specific task.
216270
///
217271
/// Items owned by a parent task are left untouched, since we do not own them.

include/swift/Runtime/Concurrency.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -663,7 +663,7 @@ void swift_task_localValuePop();
663663
/// Its Swift signature is
664664
///
665665
/// \code
666-
/// func _taskLocalValueGet<Key>(AsyncTask* task)
666+
/// func swift_task_localsCopyTo<Key>(AsyncTask* task)
667667
/// \endcode
668668
SWIFT_EXPORT_FROM(swift_Concurrency) SWIFT_CC(swift)
669669
void swift_task_localsCopyTo(AsyncTask* target);

stdlib/public/Concurrency/Actor.cpp

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -357,8 +357,6 @@ const char *__swift_runtime_env_useLegacyNonCrashingExecutorChecks() {
357357
#endif
358358
}
359359

360-
#pragma clang diagnostic push
361-
#pragma ide diagnostic ignored "ConstantConditionsOC"
362360
// Done this way because of the interaction with the initial value of
363361
// 'unexpectedExecutorLogLevel'
364362
bool swift_bincompat_useLegacyNonCrashingExecutorChecks() {
@@ -378,7 +376,6 @@ bool swift_bincompat_useLegacyNonCrashingExecutorChecks() {
378376

379377
return legacyMode;
380378
}
381-
#pragma clang diagnostic pop
382379

383380
// Check override of executor checking mode.
384381
static void checkIsCurrentExecutorMode(void *context) {

stdlib/public/Concurrency/Task.cpp

Lines changed: 53 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -729,17 +729,16 @@ swift_task_create_commonImpl(size_t rawTaskCreateFlags,
729729
assert(initialContextSize >= sizeof(FutureAsyncContext));
730730
}
731731

732-
// Add to the task group, if requested.
733-
if (taskCreateFlags.addPendingGroupTaskUnconditionally()) {
734-
assert(group && "Missing group");
735-
swift_taskGroup_addPending(group, /*unconditionally=*/true);
736-
}
737-
738-
AsyncTask *parent = nullptr;
739732
AsyncTask *currentTask = swift_task_getCurrent();
740-
if (jobFlags.task_isChildTask()) {
741-
parent = currentTask;
742-
assert(parent != nullptr && "creating a child task with no active task");
733+
AsyncTask *parent = jobFlags.task_isChildTask() ? currentTask : nullptr;
734+
735+
if (group) {
736+
assert(parent && "a task created in a group must be a child task");
737+
// Add to the task group, if requested.
738+
if (taskCreateFlags.addPendingGroupTaskUnconditionally()) {
739+
assert(group && "Missing group");
740+
swift_taskGroup_addPending(group, /*unconditionally=*/true);
741+
}
743742
}
744743

745744
// Start with user specified priority at creation time (if any)
@@ -983,12 +982,51 @@ swift_task_create_commonImpl(size_t rawTaskCreateFlags,
983982
// In a task group we would not have allowed the `add` to create a child anymore,
984983
// however better safe than sorry and `async let` are not expressed as task groups,
985984
// so they may have been spawned in any case still.
986-
if (swift_task_isCancelled(parent) ||
987-
(group && group->isCancelled()))
985+
if ((group && group->isCancelled()) || swift_task_isCancelled(parent))
988986
swift_task_cancel(task);
989987

990-
// Initialize task locals with a link to the parent task.
991-
task->_private().Local.initializeLinkParent(task, parent);
988+
// Initialize task locals storage
989+
bool taskLocalStorageInitialized = false;
990+
991+
// Inside a task group, we may have to perform some defensive copying,
992+
// check if doing so is necessary, and initialize storage using partial
993+
// defensive copies if necessary.
994+
if (group) {
995+
assert(parent && "a task created in a group must be a child task");
996+
// We are a child task in a task group; and it may happen that we are calling
997+
// addTask specifically in such shape:
998+
//
999+
// $local.withValue(theValue) { addTask {} }
1000+
//
1001+
// If this is the case, we MUST copy `theValue` (and any other such directly
1002+
// wrapping the addTask value bindings), because those values will be popped
1003+
// when withValue returns - breaking our structured concurrency guarantees
1004+
// that we rely on for the "link directly to parent's task local Item".
1005+
//
1006+
// Values set outside the task group are not subject to this problem, as
1007+
// their structural lifetime guarantee is upheld by the group scope
1008+
// out-living any addTask created tasks.
1009+
auto ParentLocal = parent->_private().Local;
1010+
// If we were going to copy ALL values anyway, we don't need to
1011+
// perform this defensive partial copying. In practice, we currently
1012+
// do not have child tasks which force copying, but we could.
1013+
assert(!taskCreateFlags.copyTaskLocals() &&
1014+
"Currently we don't have child tasks which force copying task "
1015+
"locals; unexpected attempt to combine the two!");
1016+
1017+
if (auto taskLocalHeadLinkType = ParentLocal.peekHeadLinkType()) {
1018+
if (taskLocalHeadLinkType ==
1019+
swift::TaskLocal::NextLinkType::IsNextCreatedInTaskGroupBody) {
1020+
ParentLocal.copyToOnlyOnlyFromCurrentGroup(task);
1021+
taskLocalStorageInitialized = true;
1022+
}
1023+
}
1024+
}
1025+
1026+
if (!taskLocalStorageInitialized) {
1027+
// just initialize the storage normally
1028+
task->_private().Local.initializeLinkParent(task, parent);
1029+
}
9921030
}
9931031

9941032
// Configure the initial context.
@@ -1050,7 +1088,7 @@ swift_task_create_commonImpl(size_t rawTaskCreateFlags,
10501088
#endif
10511089
swift_retain(task);
10521090
task->flagAsAndEnqueueOnExecutor(
1053-
serialExecutor); // FIXME: pass the task executor explicitly?
1091+
serialExecutor);
10541092
}
10551093

10561094
return {task, initialContext};

0 commit comments

Comments
 (0)