Skip to content

Commit 6b01fed

Browse files
committed
[Concurrency] Retain the actor around the CAS in enqueue() when necessary.
It's possible that the job we enqueue holds the last strong reference to the actor. If that job runs on another thread after we enqueue it, then it's possible for `this` to be destroyed while we're still in this function. We need to use `this` after the enqueue when the priorities don't match. When it looks like that will happen, retain `this` before the enqueue to ensure it stays alive until we're done with it. Introduce a defensive retain helper class that makes it easy to do a single retain under certain conditions even in a loop, and does RAII to balance it with a release when the scope exits. rdar://135400933
1 parent 700493e commit 6b01fed

File tree

2 files changed

+73
-3
lines changed

2 files changed

+73
-3
lines changed

include/swift/Runtime/HeapObject.h

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -386,6 +386,57 @@ class SwiftRAII {
386386
HeapObject *operator *() const { return object; }
387387
};
388388

389+
/// RAII object that wraps a Swift object and optionally performs a single
390+
/// retain on that object. Multiple requests to retain the object only perform a
391+
/// single retain, and if that retain has been done then it's automatically
392+
/// released when leaving the scope. This helps implement a defensive retain
393+
/// pattern where you may need to retain an object in some circumstances. This
394+
/// helper makes it easy to retain the object only once even when loops are
395+
/// involved, and do a release to balance the retain on all paths out of the
396+
/// scope.
397+
class SwiftDefensiveRetainRAII {
398+
HeapObject *object;
399+
bool didRetain;
400+
401+
public:
402+
// Noncopyable.
403+
SwiftDefensiveRetainRAII(const SwiftDefensiveRetainRAII &) = delete;
404+
SwiftDefensiveRetainRAII &operator=(const SwiftDefensiveRetainRAII &) = delete;
405+
406+
/// Create a new helper with the given object. The object is not retained
407+
/// initially.
408+
SwiftDefensiveRetainRAII(HeapObject *object)
409+
: object(object), didRetain(false) {}
410+
411+
~SwiftDefensiveRetainRAII() {
412+
if (didRetain)
413+
swift_release(object);
414+
}
415+
416+
/// Perform a defensive retain of the object. If a defensive retain has
417+
/// already been performed, this is a no-op.
418+
void defensiveRetain() {
419+
if (!didRetain) {
420+
swift_retain(object);
421+
didRetain = true;
422+
}
423+
}
424+
425+
/// Take the retain from the helper. This is an optimization for code paths
426+
/// that want to retain the object long-term, and avoids doing a redundant
427+
/// retain/release pair. If a defensive retain has not been done, then this
428+
/// will retain the object, so the caller always gets a +1 on the object.
429+
void takeRetain() {
430+
if (!didRetain)
431+
swift_retain(object);
432+
didRetain = false;
433+
}
434+
435+
/// Returns true if the object was defensively retained (and takeRetain not
436+
/// called). Intended for use in asserts.
437+
bool isRetained() { return didRetain; }
438+
};
439+
389440
/*****************************************************************************/
390441
/**************************** UNOWNED REFERENCES *****************************/
391442
/*****************************************************************************/

stdlib/public/Concurrency/Actor.cpp

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1357,6 +1357,7 @@ void DefaultActorImpl::enqueue(Job *job, JobPriority priority) {
13571357
bool distributedActorIsRemote = swift_distributed_actor_is_remote(this);
13581358

13591359
auto oldState = _status().load(std::memory_order_relaxed);
1360+
SwiftDefensiveRetainRAII thisRetainHelper{this};
13601361
while (true) {
13611362
auto newState = oldState;
13621363

@@ -1386,15 +1387,29 @@ void DefaultActorImpl::enqueue(Job *job, JobPriority priority) {
13861387
if (needsScheduling || needsStealer)
13871388
taskExecutor = TaskExecutorRef::fromTaskExecutorPreference(job);
13881389

1390+
// In some cases (we aren't scheduling the actor and priorities don't
1391+
// match) then we need to access `this` after the enqueue. But the enqueue
1392+
// can cause the job to run and release `this`, so we need to retain `this`
1393+
// in those cases. The conditional here matches the conditions where we can
1394+
// get to the code below that uses `this`.
1395+
bool willSchedule = !oldState.isScheduled() && newState.isScheduled();
1396+
bool priorityMismatch = oldState.getMaxPriority() != newState.getMaxPriority();
1397+
if (!willSchedule && priorityMismatch)
1398+
thisRetainHelper.defensiveRetain();
1399+
13891400
// This needs to be a store release so that we also publish the contents of
13901401
// the new Job we are adding to the atomic job queue. Pairs with consume
13911402
// in drainOne.
13921403
if (_status().compare_exchange_weak(oldState, newState,
13931404
/* success */ std::memory_order_release,
13941405
/* failure */ std::memory_order_relaxed)) {
13951406
// NOTE: `job` is off limits after this point, as another thread might run
1396-
// and destroy it now that it's enqueued.
1407+
// and destroy it now that it's enqueued. `this` is only accessible if
1408+
// `retainedThis` is true.
1409+
job = nullptr; // Ensure we can't use it accidentally.
13971410

1411+
// NOTE: only the pointer value of `this` is used here, so this one
1412+
// doesn't need a retain.
13981413
traceActorStateTransition(this, oldState, newState, distributedActorIsRemote);
13991414

14001415
if (!oldState.isScheduled() && newState.isScheduled()) {
@@ -1405,6 +1420,9 @@ void DefaultActorImpl::enqueue(Job *job, JobPriority priority) {
14051420

14061421
#if SWIFT_CONCURRENCY_ENABLE_PRIORITY_ESCALATION
14071422
if (oldState.getMaxPriority() != newState.getMaxPriority()) {
1423+
// We still need `this`, assert that we did a defensive retain.
1424+
assert(thisRetainHelper.isRetained());
1425+
14081426
if (newState.isRunning()) {
14091427
// Actor is running on a thread, escalate the thread running it
14101428
SWIFT_TASK_DEBUG_LOG("[Override] Escalating actor %p which is running on %#x to %#x priority", this, newState.currentDrainer(), priority);
@@ -1414,11 +1432,12 @@ void DefaultActorImpl::enqueue(Job *job, JobPriority priority) {
14141432
} else {
14151433
// We are scheduling a stealer for an actor due to priority override.
14161434
// This extra processing job has a reference on the actor. See
1417-
// ownership rule (2).
1435+
// ownership rule (2). That means that we need to retain `this`, which
1436+
// we'll take from the retain helper.
1437+
thisRetainHelper.takeRetain();
14181438
SWIFT_TASK_DEBUG_LOG(
14191439
"[Override] Scheduling a stealer for actor %p at %#x priority",
14201440
this, newState.getMaxPriority());
1421-
swift_retain(this);
14221441

14231442
scheduleActorProcessJob(newState.getMaxPriority(), taskExecutor);
14241443
}

0 commit comments

Comments
 (0)