-
Notifications
You must be signed in to change notification settings - Fork 10.5k
[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
[Concurrency] Defensive copying of task locals in task groups, rather than crashing #73978
Conversation
@swift-ci please test |
These are something CLion understands and suggests, but yeah let's avoid having warnings in the build.
16b3fb0
to
b99b37a
Compare
@swift-ci please test |
2facfe1
to
d713aef
Compare
@swift-ci please test |
@@ -378,7 +376,6 @@ bool swift_bincompat_useLegacyNonCrashingExecutorChecks() { | |||
|
|||
return legacyMode; | |||
} | |||
#pragma clang diagnostic pop |
There was a problem hiding this comment.
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
[Concurrency] Implement only-when-necessary defensive TL copying in in task groups
b713d76
to
30e1c69
Compare
@swift-ci please test |
test/Concurrency/Runtime/async_task_locals_in_task_group_may_need_to_copy.swift
Show resolved
Hide resolved
@swift-ci please smoke test |
resolves #73217 |
@swift-ci please smoke test macOS |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So, this is eagerly copying everything that was pushed inside of any task group, right? That's actually subtly less than optimal in the case where there are multiple task groups active at once — we only really need to copy up to the task group that the task is being created within. It's fine to do that for now, but let's not do anything that commits us to that in the long term, like making anything about this public ABI. (This doesn't need to be public anyway — it's all internal to the concurrency library.)
/// 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, | ||
}; |
There was a problem hiding this comment.
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".
There was a problem hiding this comment.
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 🤔
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!"); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
cleaned up in #74089
if (taskCreateFlags.addPendingGroupTaskUnconditionally()) { | ||
assert(group && "Missing group"); | ||
swift_taskGroup_addPending(group, /*unconditionally=*/true); | ||
} |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
/// func swift_task_localsCopyToTaskGroupChildTaskDefensively<Key>(AsyncTask* task) | ||
/// \endcode | ||
SWIFT_EXPORT_FROM(swift_Concurrency) SWIFT_CC(swift) | ||
void swift_task_localsCopyToTaskGroupChildTaskDefensively(AsyncTask* target); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
removed in #74089
Thanks for the review John!
I'll make sure we don't lock ourselfs out of the optimization, following up now |
@@ -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); |
There was a problem hiding this comment.
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
This is a more advanced version of #73881
This addresses a memory safety issue that task groups and task locals have faced since their introduction and has been previously guarded against by crashing rather than allowing the unsafety. The very specific situation this is about is the following:
The problem is that the withValue is effectively a push and a pop of the task local binding. The and child tasks refer directly to their parent's task local storage in order to minimize copying. This is fantastic because operations such as this:
will cause zero copying. (good and safe)
However, the case of DIRECTLY wrapping an addTask with a task local binding like this:
TL.$two.withValue(2222) { group.addTask {} }
is unsafe because the "pop" of the $two binding would happen BEFORE the child task gets to run to completion -- would would allow it to refer to released memory.Previous solution:
We previously would crash whenever
withValue
was used inside a task group at all. Which led to problematic and unexpected crashes when more complex libraries which wrap code in task groups would call to user code or set task locals, causing people to work around issues: swiftlang/swift-testing#339 and a few more in server libraries.Sometimes they may not be able to work around these incompatibilities though, leading to a not well composing structured task hierarchy (bad!)
This solution:
We instead now detect task locals pushed during the body of a task group. Then, as we
addTask
we defensively COPY them if directly in the parent task. This way the following situation is safe:etc.
TL;DR:
withValue { addTask {} }
toaddTask { withValue {} }
which causes no copying.Result: Instead of crashing, we now can support this pattern - which sometimes comes up unbeknownst to actual developers, because they may be using libraries which wrap them in task groups and whatnot.
resolves rdar://127156303