Skip to content

Commit c9917e3

Browse files
authored
Merge pull request #80165 from mikeash/task-group-cancellation-lock-fix-6.1
[6.1][Concurrency] Fix a race when using cancelAll on a task group concurrently with child tasks being removed.
2 parents bc506a0 + fb4eb51 commit c9917e3

File tree

4 files changed

+66
-36
lines changed

4 files changed

+66
-36
lines changed

include/swift/ABI/TaskStatus.h

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -133,10 +133,13 @@ class ChildTaskStatusRecord : public TaskStatusRecord {
133133
/// Group child tasks DO NOT have their own `ChildTaskStatusRecord` entries,
134134
/// and are only tracked by their respective `TaskGroupTaskStatusRecord`.
135135
class TaskGroupTaskStatusRecord : public TaskStatusRecord {
136-
public:
137-
AsyncTask *FirstChild;
136+
// FirstChild may be read concurrently to check for the presence of children,
137+
// so it needs to be atomic. The pointer is never dereferenced in that case,
138+
// so we can universally use memory_order_relaxed on it.
139+
std::atomic<AsyncTask *> FirstChild;
138140
AsyncTask *LastChild;
139141

142+
public:
140143
TaskGroupTaskStatusRecord()
141144
: TaskStatusRecord(TaskStatusRecordKind::TaskGroup),
142145
FirstChild(nullptr),
@@ -155,7 +158,9 @@ class TaskGroupTaskStatusRecord : public TaskStatusRecord {
155158
/// Return the first child linked by this record. This may be null;
156159
/// if not, it (and all of its successors) are guaranteed to satisfy
157160
/// `isChildTask()`.
158-
AsyncTask *getFirstChild() const { return FirstChild; }
161+
AsyncTask *getFirstChild() const {
162+
return FirstChild.load(std::memory_order_relaxed);
163+
}
159164

160165
/// Attach the passed in `child` task to this group.
161166
void attachChild(AsyncTask *child) {
@@ -165,9 +170,9 @@ class TaskGroupTaskStatusRecord : public TaskStatusRecord {
165170
auto oldLastChild = LastChild;
166171
LastChild = child;
167172

168-
if (!FirstChild) {
173+
if (!getFirstChild()) {
169174
// This is the first child we ever attach, so store it as FirstChild.
170-
FirstChild = child;
175+
FirstChild.store(child, std::memory_order_relaxed);
171176
return;
172177
}
173178

@@ -176,15 +181,18 @@ class TaskGroupTaskStatusRecord : public TaskStatusRecord {
176181

177182
void detachChild(AsyncTask *child) {
178183
assert(child && "cannot remove a null child from group");
179-
if (FirstChild == child) {
180-
FirstChild = getNextChildTask(child);
181-
if (FirstChild == nullptr) {
184+
185+
AsyncTask *prev = getFirstChild();
186+
187+
if (prev == child) {
188+
AsyncTask *next = getNextChildTask(child);
189+
FirstChild.store(next, std::memory_order_relaxed);
190+
if (next == nullptr) {
182191
LastChild = nullptr;
183192
}
184193
return;
185194
}
186195

187-
AsyncTask *prev = FirstChild;
188196
// Remove the child from the linked list, i.e.:
189197
// prev -> afterPrev -> afterChild
190198
// ==

stdlib/public/Concurrency/TaskGroup.cpp

Lines changed: 12 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -473,9 +473,8 @@ class TaskGroupBase : public TaskGroupTaskStatusRecord {
473473
bool statusCancel();
474474

475475
/// Cancel the group and all of its child tasks recursively.
476-
/// This also sets
477-
bool cancelAll();
478-
476+
/// This also sets the cancelled bit in the group status.
477+
bool cancelAll(AsyncTask *task);
479478
};
480479

481480
#if !SWIFT_CONCURRENCY_EMBEDDED
@@ -1370,7 +1369,8 @@ void DiscardingTaskGroup::offer(AsyncTask *completedTask, AsyncContext *context)
13701369
// Discarding results mode immediately treats a child failure as group cancellation.
13711370
// "All for one, one for all!" - any task failing must cause the group and all sibling tasks to be cancelled,
13721371
// such that the discarding group can exit as soon as possible.
1373-
cancelAll();
1372+
auto parent = completedTask->childFragment()->getParent();
1373+
cancelAll(parent);
13741374

13751375
if (afterComplete.hasWaitingTask() && afterComplete.pendingTasks(this) == 0) {
13761376
// We grab the waiting task while holding the group lock, because this
@@ -2089,10 +2089,12 @@ static bool swift_taskGroup_isCancelledImpl(TaskGroup *group) {
20892089

20902090
SWIFT_CC(swift)
20912091
static void swift_taskGroup_cancelAllImpl(TaskGroup *group) {
2092-
asBaseImpl(group)->cancelAll();
2092+
// TaskGroup is not a Sendable type, so this can only be called from the
2093+
// owning task.
2094+
asBaseImpl(group)->cancelAll(swift_task_getCurrent());
20932095
}
20942096

2095-
bool TaskGroupBase::cancelAll() {
2097+
bool TaskGroupBase::cancelAll(AsyncTask *owningTask) {
20962098
SWIFT_TASK_DEBUG_LOG("cancel all tasks in group = %p", this);
20972099

20982100
// Flag the task group itself as cancelled. If this was already
@@ -2106,8 +2108,8 @@ bool TaskGroupBase::cancelAll() {
21062108

21072109
// Cancel all the child tasks. TaskGroup is not a Sendable type,
21082110
// so cancelAll() can only be called from the owning task. This
2109-
// satisfies the precondition on cancelAllChildren().
2110-
_swift_taskGroup_cancelAllChildren(asAbstract(this));
2111+
// satisfies the precondition on cancelAllChildren_unlocked().
2112+
_swift_taskGroup_cancelAllChildren_unlocked(asAbstract(this), owningTask);
21112113

21122114
return true;
21132115
}
@@ -2116,22 +2118,8 @@ SWIFT_CC(swift)
21162118
static void swift_task_cancel_group_child_tasksImpl(TaskGroup *group) {
21172119
// TaskGroup is not a Sendable type, and so this operation (which is not
21182120
// currently exposed in the API) can only be called from the owning
2119-
// task. This satisfies the precondition on cancelAllChildren().
2120-
_swift_taskGroup_cancelAllChildren(group);
2121-
}
2122-
2123-
/// Cancel all the children of the given task group.
2124-
///
2125-
/// The caller must guarantee that this is either called from the
2126-
/// owning task of the task group or while holding the owning task's
2127-
/// status record lock.
2128-
void swift::_swift_taskGroup_cancelAllChildren(TaskGroup *group) {
2129-
// Because only the owning task of the task group can modify the
2130-
// child list of a task group status record, and it can only do so
2131-
// while holding the owning task's status record lock, we do not need
2132-
// any additional synchronization within this function.
2133-
for (auto childTask: group->getTaskRecord()->children())
2134-
swift_task_cancel(childTask);
2121+
// task. This satisfies the precondition on cancelAllChildren_unlocked().
2122+
_swift_taskGroup_cancelAllChildren_unlocked(group, swift_task_getCurrent());
21352123
}
21362124

21372125
// =============================================================================

stdlib/public/Concurrency/TaskPrivate.h

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -90,11 +90,16 @@ AsyncTask *_swift_task_setCurrent(AsyncTask *newTask);
9090

9191
/// Cancel all the child tasks that belong to `group`.
9292
///
93-
/// The caller must guarantee that this is either called from the
94-
/// owning task of the task group or while holding the owning task's
95-
/// status record lock.
93+
/// The caller must guarantee that this is called while holding the owning
94+
/// task's status record lock.
9695
void _swift_taskGroup_cancelAllChildren(TaskGroup *group);
9796

97+
/// Cancel all the child tasks that belong to `group`.
98+
///
99+
/// The caller must guarantee that this is called from the owning task.
100+
void _swift_taskGroup_cancelAllChildren_unlocked(TaskGroup *group,
101+
AsyncTask *owningTask);
102+
98103
/// Remove the given task from the given task group.
99104
///
100105
/// This is an internal API; clients outside of the TaskGroup implementation

stdlib/public/Concurrency/TaskStatus.cpp

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -856,6 +856,35 @@ void swift::_swift_taskGroup_detachChild(TaskGroup *group,
856856
});
857857
}
858858

859+
/// Cancel all the child tasks that belong to `group`.
860+
///
861+
/// The caller must guarantee that this is called while holding the owning
862+
/// task's status record lock.
863+
void swift::_swift_taskGroup_cancelAllChildren(TaskGroup *group) {
864+
// Because only the owning task of the task group can modify the
865+
// child list of a task group status record, and it can only do so
866+
// while holding the owning task's status record lock, we do not need
867+
// any additional synchronization within this function.
868+
for (auto childTask : group->getTaskRecord()->children())
869+
swift_task_cancel(childTask);
870+
}
871+
872+
/// Cancel all the child tasks that belong to `group`.
873+
///
874+
/// The caller must guarantee that this is called from the owning task.
875+
void swift::_swift_taskGroup_cancelAllChildren_unlocked(TaskGroup *group,
876+
AsyncTask *owningTask) {
877+
// Early out. If there are no children, there's nothing to do. We can safely
878+
// check this without locking, since this can only be concurrently mutated
879+
// from a child task. If there are no children then no more can be added.
880+
if (!group->getTaskRecord()->getFirstChild())
881+
return;
882+
883+
withStatusRecordLock(owningTask, [&group](ActiveTaskStatus status) {
884+
_swift_taskGroup_cancelAllChildren(group);
885+
});
886+
}
887+
859888
/**************************************************************************/
860889
/****************************** CANCELLATION ******************************/
861890
/**************************************************************************/

0 commit comments

Comments
 (0)