Skip to content

Commit 2a5d6a9

Browse files
authored
Merge pull request #61054 from mikeash/tsan-fix-task-cancellation
[Concurrency] Fix memory ordering around task cancellation.
2 parents baf4c36 + 9c849e4 commit 2a5d6a9

File tree

2 files changed

+63
-2
lines changed

2 files changed

+63
-2
lines changed

stdlib/public/Concurrency/TaskStatus.cpp

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -206,8 +206,11 @@ template <class Fn>
206206
static bool withStatusRecordLock(AsyncTask *task,
207207
LockContext lockContext,
208208
Fn &&fn) {
209+
auto loadOrdering = getLoadOrdering(lockContext);
209210
ActiveTaskStatus status =
210-
task->_private()._status().load(getLoadOrdering(lockContext));
211+
task->_private()._status().load(loadOrdering);
212+
if (loadOrdering == std::memory_order_acquire)
213+
_swift_tsan_acquire(task);
211214
return withStatusRecordLock(task, lockContext, status, [&] {
212215
fn(status);
213216
});
@@ -242,6 +245,7 @@ bool swift::addStatusRecord(
242245
// We have to use a release on success to make the initialization of
243246
// the new record visible to an asynchronous thread trying to modify the
244247
// status records
248+
_swift_tsan_release(task);
245249
if (task->_private()._status().compare_exchange_weak(oldStatus, newStatus,
246250
/*success*/ std::memory_order_release,
247251
/*failure*/ std::memory_order_relaxed)) {
@@ -460,9 +464,19 @@ static void swift_task_cancelImpl(AsyncTask *task) {
460464
// Set cancelled bit even if oldStatus.isStatusRecordLocked()
461465
newStatus = oldStatus.withCancelled();
462466

467+
// Perform an acquire operation on success, which pairs with the release
468+
// operation in addStatusRecord. This ensures that the contents of the
469+
// status records are visible to this thread, as well as the contents of any
470+
// cancellation handlers and the data they access. We place this acquire
471+
// barrier here, because the subsequent call to `withStatusRecordLock` might
472+
// not have its own acquire barrier. We're calling the four-argument version
473+
// which relies on the caller to have performed the first load, and if the
474+
// compare_exchange operation in withStatusRecordLock succeeds the first
475+
// time, then it won't perform an acquire.
463476
if (task->_private()._status().compare_exchange_weak(oldStatus, newStatus,
464-
/*success*/ std::memory_order_relaxed,
477+
/*success*/ std::memory_order_acquire,
465478
/*failure*/ std::memory_order_relaxed)) {
479+
_swift_tsan_acquire(task);
466480
break;
467481
}
468482
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
// RUN: %target-run-simple-swift( -Xfrontend -disable-availability-checking -parse-as-library -sanitize=thread)
2+
3+
// REQUIRES: executable_test
4+
// REQUIRES: concurrency
5+
// REQUIRES: libdispatch
6+
// REQUIRES: tsan_runtime
7+
// UNSUPPORTED: back_deployment_runtime
8+
// UNSUPPORTED: use_os_stdlib
9+
10+
@main
11+
public struct TSANDataRaceOnCancel {
12+
public static func main() async throws {
13+
for _ in 0 ..< 100 {
14+
try await withThrowingTaskGroup(of: Void.self) { group in
15+
group.addTask {
16+
let thing = Thing(otherThing: OtherThing())
17+
18+
try await withTaskCancellationHandler {
19+
try await Task.sleep(nanoseconds: 100_000_000_000)
20+
} onCancel: {
21+
thing.cancel()
22+
}
23+
}
24+
25+
// Wait a little bit so the task is scheduled before cancelling.
26+
try await Task.sleep(nanoseconds: 10_000)
27+
group.cancelAll()
28+
}
29+
}
30+
}
31+
}
32+
33+
final class Thing: Sendable {
34+
private let otherThing: OtherThing
35+
36+
init(otherThing: OtherThing) {
37+
self.otherThing = otherThing // Write of size 8 by thread Y
38+
}
39+
40+
func cancel() {
41+
self.otherThing.cancel() // Read of size 8 by thread X
42+
}
43+
}
44+
45+
final class OtherThing: Sendable {
46+
func cancel() {}
47+
}

0 commit comments

Comments
 (0)