Skip to content

Commit 763666e

Browse files
authored
Merge pull request #77031 from mikeash/actor-enqueue-retain
[Concurrency] Retain the actor around the CAS in enqueue() when necessary.
2 parents 0d9e62e + 6b01fed commit 763666e

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

13611361
auto oldState = _status().load(std::memory_order_relaxed);
1362+
SwiftDefensiveRetainRAII thisRetainHelper{this};
13621363
while (true) {
13631364
auto newState = oldState;
13641365

@@ -1388,15 +1389,29 @@ void DefaultActorImpl::enqueue(Job *job, JobPriority priority) {
13881389
if (needsScheduling || needsStealer)
13891390
taskExecutor = TaskExecutorRef::fromTaskExecutorPreference(job);
13901391

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

1413+
// NOTE: only the pointer value of `this` is used here, so this one
1414+
// doesn't need a retain.
14001415
traceActorStateTransition(this, oldState, newState, distributedActorIsRemote);
14011416

14021417
if (!oldState.isScheduled() && newState.isScheduled()) {
@@ -1407,6 +1422,9 @@ void DefaultActorImpl::enqueue(Job *job, JobPriority priority) {
14071422

14081423
#if SWIFT_CONCURRENCY_ENABLE_PRIORITY_ESCALATION
14091424
if (oldState.getMaxPriority() != newState.getMaxPriority()) {
1425+
// We still need `this`, assert that we did a defensive retain.
1426+
assert(thisRetainHelper.isRetained());
1427+
14101428
if (newState.isRunning()) {
14111429
// Actor is running on a thread, escalate the thread running it
14121430
SWIFT_TASK_DEBUG_LOG("[Override] Escalating actor %p which is running on %#x to %#x priority", this, newState.currentDrainer(), priority);
@@ -1416,11 +1434,12 @@ void DefaultActorImpl::enqueue(Job *job, JobPriority priority) {
14161434
} else {
14171435
// We are scheduling a stealer for an actor due to priority override.
14181436
// This extra processing job has a reference on the actor. See
1419-
// ownership rule (2).
1437+
// ownership rule (2). That means that we need to retain `this`, which
1438+
// we'll take from the retain helper.
1439+
thisRetainHelper.takeRetain();
14201440
SWIFT_TASK_DEBUG_LOG(
14211441
"[Override] Scheduling a stealer for actor %p at %#x priority",
14221442
this, newState.getMaxPriority());
1423-
swift_retain(this);
14241443

14251444
scheduleActorProcessJob(newState.getMaxPriority(), taskExecutor);
14261445
}

0 commit comments

Comments
 (0)