Skip to content

[Concurrency] Defensive copying of task locals in task groups, rather than crashing #73978

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

Merged
54 changes: 51 additions & 3 deletions include/swift/ABI/TaskLocal.h
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,14 @@ class TaskLocal {
/// lookups by skipping empty parent tasks during get(), and explained
/// in depth in `createParentLink`.
IsParent = 0b01,
/// The task local binding was created inside the body of a `withTaskGroup`,
/// and therefore must either copy it, or crash when a child task is created
/// using 'group.addTask' and it would refer to this task local.
///
/// Items of this kind must be copied by a group child task for access
/// safety reasons, as otherwise the pop would happen before the child task
/// has completed.
IsNextCreatedInTaskGroupBody = 0b10,
};
Copy link
Contributor

Choose a reason for hiding this comment

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

You don't need to do this now, but it kind of sounds like these are flags instead of a single "kind".

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Hm, lemme think if I can put these somewhere else 🤔


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

static Item *createLink(AsyncTask *task,
const HeapObject *key,
const Metadata *valueType,
bool inTaskGroupBody);

static Item *createLink(AsyncTask *task,
const HeapObject *key,
const Metadata *valueType);

static Item *createLinkInTaskGroup(
AsyncTask *task,
const HeapObject *key,
const Metadata *valueType);

void destroy(AsyncTask *task);

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

void relinkNext(Item* nextOverride) {
assert(!getNext() &&
"Can only relink task local item that was not pointing at anything yet");
assert(nextOverride->isNextLinkPointer() ||
nextOverride->isParentPointer() &&
"Currently relinking is only done within a task group to "
"avoid within-taskgroup next pointers; attempted to point at "
"task local declared within task group body though!");
Copy link
Contributor

Choose a reason for hiding this comment

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

A || B && C is parsed as A || (B && C). That turns out to be logically equivalent in this case, but still, it's better to get into the habit of parenthesizing assertion conditions.

The assertion and behavior here is strange — the method name suggests that it's just changing the next pointer, but you're really assuming a stronger condition. It feels like you can't really assert that condition properly, which to me is usually a sign that the operation isn't as general as I'm thinking it is. That's okay; operations don't always generalize. It just suggests that you ought to name this method something that indicates its special behavior, and maybe feel less bad about writing something that doesn't feel like a generic operation.

I assume what we're doing here is cloning the entries and then calling relinkNext on them.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think it's a good point to rename this method, it is a very specific trick. When we detect the "next" task local item is "safe" as in "not the bad pattern where we have to copy" we immediately link to it rather than continuing our copying dance. I'll rename and see if I can clarify some more.

Fixing the assertion too, thank you for noticing that

Copy link
Contributor Author

Choose a reason for hiding this comment

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

cleaned up in #74089


next = reinterpret_cast<uintptr_t>(nextOverride) |
static_cast<uintptr_t>((nextOverride->isNextLinkPointer()
? NextLinkType::IsNextCreatedInTaskGroupBody
: NextLinkType::IsParent));
}

NextLinkType getNextLinkType() const {
return static_cast<NextLinkType>(next & statusMask);
}

bool isNextLinkPointer() const {
return static_cast<NextLinkType>(next & statusMask) ==
NextLinkType::IsNext;
}

bool isNextLinkPointerCreatedInTaskGroupBody() const {
return static_cast<NextLinkType>(next & statusMask) ==
NextLinkType::IsNextCreatedInTaskGroupBody;
}

/// Item does not contain any actual value, and is only used to point at
/// a specific parent item.
bool isEmpty() const {
Expand All @@ -127,7 +170,7 @@ class TaskLocal {
reinterpret_cast<char *>(this) + storageOffset(valueType));
}

void copyTo(AsyncTask *task);
TaskLocal::Item* copyTo(AsyncTask *task);

/// Compute the offset of the storage from the base of the item.
static size_t storageOffset(const Metadata *valueType) {
Expand All @@ -136,9 +179,9 @@ class TaskLocal {
if (valueType) {
size_t alignment = valueType->vw_alignment();
return (offset + alignment - 1) & ~(alignment - 1);
} else {
return offset;
}

return offset;
}

/// Determine the size of the item given a particular value type.
Expand Down Expand Up @@ -200,6 +243,9 @@ class TaskLocal {
/// can be safely disposed of.
bool popValue(AsyncTask *task);

/// Peek at the head item and get its type.
std::optional<NextLinkType> peekHeadLinkType() const;

/// Copy all task-local bindings to the target task.
///
/// The new bindings allocate their own items and can out-live the current task.
Expand All @@ -212,6 +258,8 @@ class TaskLocal {
/// "pop" of the `B` value - it was spawned from a scope where only B was observable.
void copyTo(AsyncTask *target);

void copyToOnlyOnlyFromCurrent(AsyncTask *target);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

renamed this in follow up #74089


/// Destroy and deallocate all items stored by this specific task.
///
/// Items owned by a parent task are left untouched, since we do not own them.
Expand Down
14 changes: 13 additions & 1 deletion include/swift/Runtime/Concurrency.h
Original file line number Diff line number Diff line change
Expand Up @@ -663,11 +663,23 @@ void swift_task_localValuePop();
/// Its Swift signature is
///
/// \code
/// func _taskLocalValueGet<Key>(AsyncTask* task)
/// func swift_task_localsCopyTo<Key>(AsyncTask* task)
/// \endcode
SWIFT_EXPORT_FROM(swift_Concurrency) SWIFT_CC(swift)
void swift_task_localsCopyTo(AsyncTask* target);

/// Some task local bindings must be copied defensively when a child task is
/// created in a task group. See task creation (swift_task_create_common) for
/// a detailed discussion how and when this is used.
///
/// Its Swift signature is
///
/// \code
/// func swift_task_localsCopyToTaskGroupChildTaskDefensively<Key>(AsyncTask* task)
/// \endcode
SWIFT_EXPORT_FROM(swift_Concurrency) SWIFT_CC(swift)
void swift_task_localsCopyToTaskGroupChildTaskDefensively(AsyncTask* target);
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't think this needs to be an exported runtime function.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think we can managed without exposing it -- will remove (and remove before getting this into 6.0)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

removed in #74089


/// Switch the current task to a new executor if we aren't already
/// running on a compatible executor.
///
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -378,6 +378,12 @@ OVERRIDE_TASK_LOCAL(task_localsCopyTo, void,
(AsyncTask *target),
(target))

OVERRIDE_TASK_LOCAL(task_localsCopyToTaskGroupChildTaskDefensively, void,
SWIFT_EXPORT_FROM(swift_Concurrency), SWIFT_CC(swift),
swift::,
(AsyncTask *target),
(target))

OVERRIDE_TASK_STATUS(task_hasTaskGroupStatusRecord, bool,
SWIFT_EXPORT_FROM(swift_Concurrency), SWIFT_CC(swift),
swift::, , )
Expand Down
3 changes: 0 additions & 3 deletions stdlib/public/Concurrency/Actor.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -357,8 +357,6 @@ const char *__swift_runtime_env_useLegacyNonCrashingExecutorChecks() {
#endif
}

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

return legacyMode;
}
#pragma clang diagnostic pop
Copy link
Contributor Author

Choose a reason for hiding this comment

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

was producing warnings, i had introduced this recently -- sorry for noise


// Check override of executor checking mode.
static void checkIsCurrentExecutorMode(void *context) {
Expand Down
68 changes: 53 additions & 15 deletions stdlib/public/Concurrency/Task.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -729,17 +729,16 @@ swift_task_create_commonImpl(size_t rawTaskCreateFlags,
assert(initialContextSize >= sizeof(FutureAsyncContext));
}

// Add to the task group, if requested.
if (taskCreateFlags.addPendingGroupTaskUnconditionally()) {
assert(group && "Missing group");
swift_taskGroup_addPending(group, /*unconditionally=*/true);
}

AsyncTask *parent = nullptr;
AsyncTask *currentTask = swift_task_getCurrent();
if (jobFlags.task_isChildTask()) {
parent = currentTask;
assert(parent != nullptr && "creating a child task with no active task");
AsyncTask *parent = jobFlags.task_isChildTask() ? currentTask : nullptr;

if (group) {
assert(parent && "a task created in a group must be a child task");
// Add to the task group, if requested.
if (taskCreateFlags.addPendingGroupTaskUnconditionally()) {
assert(group && "Missing group");
swift_taskGroup_addPending(group, /*unconditionally=*/true);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

This is a logically-unrelated cleanup? Seems reasonable, just trying to find the connection.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, this bit is just cleanup -- same functionality.

}

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

// Initialize task locals with a link to the parent task.
task->_private().Local.initializeLinkParent(task, parent);
// Initialize task locals storage
bool taskLocalStorageInitialized = false;

// Inside a task group, we may have to perform some defensive copying,
// check if doing so is necessary, and initialize storage using partial
// defensive copies if necessary.
if (group) {
assert(parent && "a task created in a group must be a child task");
// We are a child task in a task group; and it may happen that we are calling
// addTask specifically in such shape:
//
// $local.withValue(theValue) { addTask {} }
//
// If this is the case, we MUST copy `theValue` (and any other such directly
// wrapping the addTask value bindings), because those values will be popped
// when withValue returns - breaking our structured concurrency guarantees
// that we rely on for the "link directly to parent's task local Item".
//
// Values set outside the task group are not subject to this problem, as
// their structural lifetime guarantee is upheld by the group scope
// out-living any addTask created tasks.
auto ParentLocal = parent->_private().Local;
// If we were going to copy ALL values anyway, we don't need to
// perform this defensive partial copying. In practice, we currently
// do not have child tasks which force copying, but we could.
assert(!taskCreateFlags.copyTaskLocals() &&
"Currently we don't have child tasks which force copying task "
"locals; unexpected attempt to combine the two!");

if (auto taskLocalHeadLinkType = ParentLocal.peekHeadLinkType()) {
if (taskLocalHeadLinkType ==
swift::TaskLocal::NextLinkType::IsNextCreatedInTaskGroupBody) {
swift_task_localsCopyToTaskGroupChildTaskDefensively(task);
taskLocalStorageInitialized = true;
}
}
}

if (!taskLocalStorageInitialized) {
// just initialize the storage normally
task->_private().Local.initializeLinkParent(task, parent);
}
}

// Configure the initial context.
Expand Down Expand Up @@ -1050,7 +1088,7 @@ swift_task_create_commonImpl(size_t rawTaskCreateFlags,
#endif
swift_retain(task);
task->flagAsAndEnqueueOnExecutor(
serialExecutor); // FIXME: pass the task executor explicitly?
serialExecutor);
}

return {task, initialContext};
Expand Down
Loading