Skip to content

Commit 8d24648

Browse files
committed
[6.0][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 (cherry picked from commit 6b01fed)
1 parent f30a16c commit 8d24648

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
@@ -1337,6 +1337,7 @@ void DefaultActorImpl::enqueue(Job *job, JobPriority priority) {
13371337
bool distributedActorIsRemote = swift_distributed_actor_is_remote(this);
13381338

13391339
auto oldState = _status().load(std::memory_order_relaxed);
1340+
SwiftDefensiveRetainRAII thisRetainHelper{this};
13401341
while (true) {
13411342
auto newState = oldState;
13421343

@@ -1366,15 +1367,29 @@ void DefaultActorImpl::enqueue(Job *job, JobPriority priority) {
13661367
if (needsScheduling || needsStealer)
13671368
taskExecutor = TaskExecutorRef::fromTaskExecutorPreference(job);
13681369

1370+
// In some cases (we aren't scheduling the actor and priorities don't
1371+
// match) then we need to access `this` after the enqueue. But the enqueue
1372+
// can cause the job to run and release `this`, so we need to retain `this`
1373+
// in those cases. The conditional here matches the conditions where we can
1374+
// get to the code below that uses `this`.
1375+
bool willSchedule = !oldState.isScheduled() && newState.isScheduled();
1376+
bool priorityMismatch = oldState.getMaxPriority() != newState.getMaxPriority();
1377+
if (!willSchedule && priorityMismatch)
1378+
thisRetainHelper.defensiveRetain();
1379+
13691380
// This needs to be a store release so that we also publish the contents of
13701381
// the new Job we are adding to the atomic job queue. Pairs with consume
13711382
// in drainOne.
13721383
if (_status().compare_exchange_weak(oldState, newState,
13731384
/* success */ std::memory_order_release,
13741385
/* failure */ std::memory_order_relaxed)) {
13751386
// NOTE: `job` is off limits after this point, as another thread might run
1376-
// and destroy it now that it's enqueued.
1387+
// and destroy it now that it's enqueued. `this` is only accessible if
1388+
// `retainedThis` is true.
1389+
job = nullptr; // Ensure we can't use it accidentally.
13771390

1391+
// NOTE: only the pointer value of `this` is used here, so this one
1392+
// doesn't need a retain.
13781393
traceActorStateTransition(this, oldState, newState, distributedActorIsRemote);
13791394

13801395
if (!oldState.isScheduled() && newState.isScheduled()) {
@@ -1385,6 +1400,9 @@ void DefaultActorImpl::enqueue(Job *job, JobPriority priority) {
13851400

13861401
#if SWIFT_CONCURRENCY_ENABLE_PRIORITY_ESCALATION
13871402
if (oldState.getMaxPriority() != newState.getMaxPriority()) {
1403+
// We still need `this`, assert that we did a defensive retain.
1404+
assert(thisRetainHelper.isRetained());
1405+
13881406
if (newState.isRunning()) {
13891407
// Actor is running on a thread, escalate the thread running it
13901408
SWIFT_TASK_DEBUG_LOG("[Override] Escalating actor %p which is running on %#x to %#x priority", this, newState.currentDrainer(), priority);
@@ -1394,11 +1412,12 @@ void DefaultActorImpl::enqueue(Job *job, JobPriority priority) {
13941412
} else {
13951413
// We are scheduling a stealer for an actor due to priority override.
13961414
// This extra processing job has a reference on the actor. See
1397-
// ownership rule (2).
1415+
// ownership rule (2). That means that we need to retain `this`, which
1416+
// we'll take from the retain helper.
1417+
thisRetainHelper.takeRetain();
13981418
SWIFT_TASK_DEBUG_LOG(
13991419
"[Override] Scheduling a stealer for actor %p at %#x priority",
14001420
this, newState.getMaxPriority());
1401-
swift_retain(this);
14021421

14031422
scheduleActorProcessJob(newState.getMaxPriority(), taskExecutor);
14041423
}

0 commit comments

Comments
 (0)