Skip to content

Commit a043ebf

Browse files
Don't switch threads when running isolated deinit of the default actor.
1 parent 9dfa174 commit a043ebf

File tree

4 files changed

+134
-61
lines changed

4 files changed

+134
-61
lines changed

stdlib/public/CompatibilityOverride/CompatibilityOverrideConcurrency.def

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,11 @@ OVERRIDE_ACTOR(task_switch, void,
142142
TaskContinuationFunction *resumeFunction, SerialExecutorRef newExecutor),
143143
(resumeToContext, resumeFunction, newExecutor))
144144

145+
OVERRIDE_ACTOR(task_performOnExecutor, void,
146+
SWIFT_EXPORT_FROM(swift_Concurrency), SWIFT_CC(swift), swift::,
147+
(void *context, AdHocWorkFunction *work, SerialExecutorRef newExecutor),
148+
(context, work, newExecutor))
149+
145150
OVERRIDE_TASK(task_create_common, AsyncTaskAndContext,
146151
SWIFT_EXPORT_FROM(swift_Concurrency), SWIFT_CC(swift), swift::,
147152
(size_t taskCreateFlags,
@@ -200,11 +205,6 @@ OVERRIDE_TASK(task_createNullaryContinuationJob, NullaryContinuationJob *,
200205
SWIFT_EXPORT_FROM(swift_Concurrency), SWIFT_CC(swift), swift::,
201206
(size_t priority,
202207
AsyncTask *continuation), (priority, continuation))
203-
204-
OVERRIDE_TASK(task_performOnExecutor, void,
205-
SWIFT_EXPORT_FROM(swift_Concurrency), SWIFT_CC(swift), swift::,
206-
(void *context, AdHocWorkFunction *work, SerialExecutorRef newExecutor),
207-
(context, work, newExecutor))
208208

209209
HOOKED_OVERRIDE_TASK_NORETURN(task_asyncMainDrainQueue,
210210
SWIFT_EXPORT_FROM(swift_Concurrency), SWIFT_CC(swift),

stdlib/public/Concurrency/Actor.cpp

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2140,6 +2140,92 @@ static void swift_task_switchImpl(SWIFT_ASYNC_CONTEXT AsyncContext *resumeContex
21402140
_swift_task_clearCurrent();
21412141
}
21422142

2143+
namespace {
2144+
/// Job that allows to use executor API to schedule a block of task-less
2145+
/// synchronous code.
2146+
class AdHocJob : public Job {
2147+
private:
2148+
void *Context;
2149+
AdHocWorkFunction *Work;
2150+
2151+
public:
2152+
AdHocJob(JobPriority priority, void *context, AdHocWorkFunction *work)
2153+
: Job({JobKind::AdHoc, priority}, &process), Context(context),
2154+
Work(work) {}
2155+
2156+
SWIFT_CC(swiftasync)
2157+
static void process(Job *_job) {
2158+
auto *job = cast<AdHocJob>(_job);
2159+
void *ctx = job->Context;
2160+
AdHocWorkFunction *work = job->Work;
2161+
delete job;
2162+
return work(ctx);
2163+
}
2164+
2165+
static bool classof(const Job *job) {
2166+
return job->Flags.getKind() == JobKind::AdHoc;
2167+
}
2168+
};
2169+
} // namespace
2170+
2171+
SWIFT_CC(swift)
2172+
static void swift_task_performOnExecutorImpl(void *context,
2173+
AdHocWorkFunction *work,
2174+
SerialExecutorRef newExecutor) {
2175+
// If the current executor is compatible with running the new executor,
2176+
// we can just immediately continue running with the resume function
2177+
// we were passed in.
2178+
//
2179+
// Note that swift_task_isCurrentExecutor() returns true for @MainActor
2180+
// when running on the main thread without any executor
2181+
if (swift_task_isCurrentExecutor(newExecutor)) {
2182+
return work(context); // 'return' forces tail call
2183+
}
2184+
2185+
// Optimize deallocation of the default actors
2186+
if (context == newExecutor.getIdentity() && newExecutor.isDefaultActor()) {
2187+
// Try to take the lock. This should always succeed, unless someone is
2188+
// running the actor using unsafe unowned reference.
2189+
if (asImpl(newExecutor.getDefaultActor())->tryLock(false)) {
2190+
2191+
// Don't unlock current executor, because we must preserve it when
2192+
// returning. If we release the lock, we might not be able to get it back.
2193+
// It cannot produce deadlocks, because:
2194+
// * we use tryLock(), not lock()
2195+
// * each object can be deinitialized only once, so call graph of
2196+
// deinit's cannot have cycles.
2197+
2198+
// Function runOnAssumedThread() tries to reuse existing tracking info,
2199+
// but we don't have a tail call anyway, so this does not help much here.
2200+
// Always create new tracking info to keep code simple.
2201+
ExecutorTrackingInfo trackingInfo;
2202+
trackingInfo.enterAndShadow(newExecutor, TaskExecutorRef::undefined());
2203+
2204+
// Run the work.
2205+
work(context);
2206+
2207+
// `work` is a synchronous function, it cannot call swift_task_switch()
2208+
// If it calls any synchronous API that may change executor inside
2209+
// tracking info, that API is also responsible for changing it back.
2210+
assert(newExecutor == trackingInfo.getActiveExecutor());
2211+
2212+
// Leave the tracking frame
2213+
trackingInfo.leave();
2214+
2215+
// Give up the current actor.
2216+
asImpl(newExecutor.getDefaultActor())->unlock(true);
2217+
return;
2218+
}
2219+
}
2220+
2221+
auto currentTask = swift_task_getCurrent();
2222+
auto priority = currentTask ? swift_task_currentPriority(currentTask)
2223+
: swift_task_getCurrentThreadPriority();
2224+
2225+
auto job = new AdHocJob(priority, context, work);
2226+
swift_task_enqueue(job, newExecutor);
2227+
}
2228+
21432229
/*****************************************************************************/
21442230
/************************* GENERIC ACTOR INTERFACES **************************/
21452231
/*****************************************************************************/

stdlib/public/Concurrency/Task.cpp

Lines changed: 0 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1674,56 +1674,6 @@ swift_task_createNullaryContinuationJobImpl(
16741674
return job;
16751675
}
16761676

1677-
/// Job that allows to use executor API to schedule a block of task-less
1678-
/// synchronous code.
1679-
class AdHocJob : public Job {
1680-
private:
1681-
void *Context;
1682-
AdHocWorkFunction *Work;
1683-
1684-
public:
1685-
AdHocJob(JobPriority priority, void *context, AdHocWorkFunction *work)
1686-
: Job({JobKind::AdHoc, priority}, &process), Context(context),
1687-
Work(work) {}
1688-
1689-
SWIFT_CC(swiftasync)
1690-
static void process(Job *_job) {
1691-
auto *job = cast<AdHocJob>(_job);
1692-
void *ctx = job->Context;
1693-
AdHocWorkFunction *work = job->Work;
1694-
delete job;
1695-
return work(ctx);
1696-
}
1697-
1698-
static bool classof(const Job *job) {
1699-
return job->Flags.getKind() == JobKind::AdHoc;
1700-
}
1701-
};
1702-
1703-
SWIFT_CC(swift)
1704-
static void swift_task_performOnExecutorImpl(void *context,
1705-
AdHocWorkFunction *work,
1706-
SerialExecutorRef newExecutor) {
1707-
auto currentExecutor = swift_task_getCurrentExecutor();
1708-
1709-
// If the current executor is compatible with running the new executor,
1710-
// we can just immediately continue running with the resume function
1711-
// we were passed in.
1712-
//
1713-
// Note that swift_task_isCurrentExecutor() returns true for @MainActor
1714-
// when running on the main thread without any executor
1715-
if (swift_task_isCurrentExecutor(newExecutor)) {
1716-
return work(context); // 'return' forces tail call
1717-
}
1718-
1719-
auto currentTask = swift_task_getCurrent();
1720-
auto priority = currentTask ? swift_task_currentPriority(currentTask)
1721-
: swift_task_getCurrentThreadPriority();
1722-
1723-
auto job = new AdHocJob(priority, context, work);
1724-
swift_task_enqueue(job, newExecutor);
1725-
}
1726-
17271677
SWIFT_CC(swift)
17281678
void swift::swift_continuation_logFailedCheck(const char *message) {
17291679
swift_reportError(0, message);

test/Concurrency/Runtime/async_task_locals_isolated_deinit.swift

Lines changed: 43 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,21 @@ actor ActorNoOp {
5555
}
5656
}
5757

58+
actor ProxyActor: Actor {
59+
private let impl: ActorNoOp
60+
61+
init(expectedNumber: Int, group: DispatchGroup) {
62+
self.impl = ActorNoOp(expectedNumber: expectedNumber, group: group)
63+
}
64+
65+
// Explicit deinit to force isolation
66+
deinit {}
67+
68+
nonisolated var unownedExecutor: UnownedSerialExecutor {
69+
return impl.unownedExecutor
70+
}
71+
}
72+
5873
@globalActor actor AnotherActor: GlobalActor {
5974
static let shared = AnotherActor()
6075

@@ -107,7 +122,7 @@ class ClassNoOp: Probe {
107122
let tests = TestSuite("Isolated Deinit")
108123

109124
if #available(SwiftStdlib 5.1, *) {
110-
tests.test("fast path") {
125+
tests.test("class sync fast path") {
111126
let group = DispatchGroup()
112127
group.enter(1)
113128
Task {
@@ -120,20 +135,42 @@ if #available(SwiftStdlib 5.1, *) {
120135
group.wait()
121136
}
122137

123-
tests.test("slow path") {
138+
tests.test("class sync slow path") {
124139
let group = DispatchGroup()
125-
group.enter(2)
140+
group.enter(1)
126141
Task {
127-
TL.$number.withValue(37) {
128-
_ = ActorNoOp(expectedNumber: 0, group: group)
129-
}
130142
TL.$number.withValue(99) {
131143
_ = ClassNoOp(expectedNumber: 0, group: group)
132144
}
133145
}
134146
group.wait()
135147
}
136148

149+
tests.test("actor sync fast path") {
150+
let group = DispatchGroup()
151+
group.enter(1)
152+
Task {
153+
TL.$number.withValue(99) {
154+
// Despite last release happening not on the actor itself,
155+
// this is still a fast path due to optimisation for deallocating actors.
156+
_ = ActorNoOp(expectedNumber: 99, group: group)
157+
}
158+
}
159+
group.wait()
160+
}
161+
162+
tests.test("actor sync slow path") {
163+
let group = DispatchGroup()
164+
group.enter(1)
165+
Task {
166+
TL.$number.withValue(99) {
167+
// Using ProxyActor breaks optimization
168+
_ = ProxyActor(expectedNumber: 0, group: group)
169+
}
170+
}
171+
group.wait()
172+
}
173+
137174
tests.test("no TLs") {
138175
let group = DispatchGroup()
139176
group.enter(2)

0 commit comments

Comments
 (0)