Skip to content

Commit ba6fa6c

Browse files
committed
[Concurrency] SerialExecutor.checkIsolated
Allow serial executors to perform a "last resort" check before crashing in calls like preconditionIsolated and assumeIsolated. This allows custom excecutors to use external information to claim that while the synchronous code is not executing in a Swift Task, it IS executing on the apropriate thread or queue the code is expecting.
1 parent 16d3997 commit ba6fa6c

16 files changed

+495
-45
lines changed

include/swift/ABI/Executor.h

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -155,8 +155,12 @@ class SerialExecutorRef {
155155
return reinterpret_cast<DefaultActor*>(Identity);
156156
}
157157

158+
bool hasSerialExecutorWitnessTable() const {
159+
return !isGeneric() && !isDefaultActor();
160+
}
161+
158162
const SerialExecutorWitnessTable *getSerialExecutorWitnessTable() const {
159-
assert(!isGeneric() && !isDefaultActor());
163+
assert(hasSerialExecutorWitnessTable());
160164
auto table = Implementation & WitnessTableMask;
161165
return reinterpret_cast<const SerialExecutorWitnessTable*>(table);
162166
}

include/swift/Runtime/Concurrency.h

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -715,6 +715,11 @@ void swift_task_enqueue(Job *job, SerialExecutorRef executor);
715715
SWIFT_EXPORT_FROM(swift_Concurrency) SWIFT_CC(swift)
716716
void swift_task_enqueueGlobal(Job *job);
717717

718+
/// Invoke an executor's `checkIsolated` or otherwise equivalent API,
719+
/// that will crash if the current executor is NOT the passed executor.
720+
SWIFT_EXPORT_FROM(swift_Concurrency) SWIFT_CC(swift)
721+
void swift_task_checkIsolated(SerialExecutorRef executor);
722+
718723
/// A count in nanoseconds.
719724
using JobDelay = unsigned long long;
720725

@@ -729,6 +734,9 @@ void swift_task_enqueueGlobalWithDeadline(long long sec, long long nsec,
729734
SWIFT_EXPORT_FROM(swift_Concurrency) SWIFT_CC(swift)
730735
void swift_task_enqueueMainExecutor(Job *job);
731736

737+
/// WARNING: This method is expected to CRASH when caller is not on the
738+
/// expected executor.
739+
///
732740
/// Return true if the caller is running in a Task on the passed Executor.
733741
SWIFT_EXPORT_FROM(swift_Concurrency) SWIFT_CC(swift)
734742
bool swift_task_isOnExecutor(
@@ -781,6 +789,12 @@ SWIFT_CC(swift) void (*swift_task_enqueueGlobalWithDeadline_hook)(
781789
int clock, Job *job,
782790
swift_task_enqueueGlobalWithDeadline_original original);
783791

792+
typedef SWIFT_CC(swift) void (*swift_task_checkIsolated_original)(SerialExecutorRef executor);
793+
SWIFT_EXPORT_FROM(swift_Concurrency)
794+
SWIFT_CC(swift) void (*swift_task_checkIsolated_hook)(
795+
SerialExecutorRef executor, swift_task_checkIsolated_original original);
796+
797+
784798
typedef SWIFT_CC(swift) bool (*swift_task_isOnExecutor_original)(
785799
HeapObject *executor,
786800
const Metadata *selfType,

stdlib/public/Concurrency/Actor.cpp

Lines changed: 80 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
#include "../CompatibilityOverride/CompatibilityOverride.h"
2323
#include "swift/ABI/Actor.h"
2424
#include "swift/ABI/Task.h"
25+
#include "TaskPrivate.h"
2526
#include "swift/Basic/ListMerger.h"
2627
#include "swift/Concurrency/Actor.h"
2728
#include "swift/Runtime/AccessibleFunction.h"
@@ -314,39 +315,107 @@ bool _task_serialExecutor_isSameExclusiveExecutionContext(
314315
const SerialExecutorWitnessTable *wtable);
315316

316317
SWIFT_CC(swift)
317-
static bool swift_task_isCurrentExecutorImpl(SerialExecutorRef executor) {
318+
static bool swift_task_isCurrentExecutorImpl(SerialExecutorRef expectedExecutor) {
318319
auto current = ExecutorTrackingInfo::current();
319320

320321
if (!current) {
321-
// TODO(ktoso): checking the "is main thread" is not correct, main executor can be not main thread, relates to rdar://106188692
322-
return executor.isMainExecutor() && isExecutingOnMainThread();
322+
// We have no current executor, i.e. we are running "outside" of Swift
323+
// Concurrency. We could still be running on a thread/queue owned by
324+
// the expected executor however, so we need to try a bit harder before
325+
// we fail.
326+
327+
// Are we expecting the main executor and are using the main thread?
328+
if (expectedExecutor.isMainExecutor() && isExecutingOnMainThread()) {
329+
// TODO: remove this special case when MainExecutor implements checkIsolated
330+
return true;
331+
}
332+
333+
// Otherwise, as last resort, let the expected executor check using
334+
// external means, as it may "know" this thread is managed by it etc.
335+
swift_task_checkIsolated(expectedExecutor);
336+
337+
// checkIsolated did not crash, so we are on the right executor, after all!
338+
return true;
323339
}
324340

325-
auto currentExecutor = current->getActiveExecutor();
326-
if (currentExecutor == executor) {
341+
SerialExecutorRef currentExecutor = current->getActiveExecutor();
342+
343+
// Fast-path: the executor is exactly the same memory address;
344+
// We assume executors do not come-and-go appearing under the same address,
345+
// and treat pointer equality of executors as good enough to assume the executor.
346+
if (currentExecutor == expectedExecutor) {
327347
return true;
328348
}
329349

330-
if (executor.isComplexEquality()) {
350+
// Fast-path, specialize the common case of comparing two main executors.
351+
if (currentExecutor.isMainExecutor() && expectedExecutor.isMainExecutor()) {
352+
return true;
353+
}
354+
355+
// If the expected executor is "default" then we should have matched
356+
// by pointer equality already with the current executor.
357+
if (expectedExecutor.isDefaultActor()) {
358+
// If the expected executor is a default actor, it makes no sense to try
359+
// the 'checkIsolated' call, it must be equal to the other actor, or it is
360+
// not the same isolation domain.
361+
swift_Concurrency_fatalError(0, "Incorrect actor executor assumption");
362+
return false;
363+
}
364+
365+
if (expectedExecutor.isMainExecutor() && !currentExecutor.isMainExecutor()) {
366+
// TODO: Invoke checkIsolated() on "main" SerialQueue once it implements `checkIsolated`, otherwise messages will be sub-par and hard to address
367+
swift_Concurrency_fatalError(0, "Incorrect actor executor assumption; Expected MainActor executor");
368+
return false;
369+
} else if (!expectedExecutor.isMainExecutor() && currentExecutor.isMainExecutor()) {
370+
// TODO: Invoke checkIsolated() on "main" SerialQueue once it implements `checkIsolated`, otherwise messages will be sub-par and hard to address
371+
swift_Concurrency_fatalError(0, "Incorrect actor executor assumption; Expected not-MainActor executor");
372+
return false;
373+
}
374+
375+
if (expectedExecutor.isComplexEquality()) {
331376
if (!swift_compareWitnessTables(
332377
reinterpret_cast<const WitnessTable*>(currentExecutor.getSerialExecutorWitnessTable()),
333-
reinterpret_cast<const WitnessTable*>(executor.getSerialExecutorWitnessTable()))) {
378+
reinterpret_cast<const WitnessTable*>(expectedExecutor.getSerialExecutorWitnessTable()))) {
334379
// different witness table, we cannot invoke complex equality call
335380
return false;
336381
}
382+
337383
// Avoid passing nulls to Swift for the isSame check:
338-
if (!currentExecutor.getIdentity() || !executor.getIdentity()) {
384+
if (!currentExecutor.getIdentity() || !expectedExecutor.getIdentity()) {
339385
return false;
340386
}
341387

342388
return _task_serialExecutor_isSameExclusiveExecutionContext(
343389
currentExecutor.getIdentity(),
344-
executor.getIdentity(),
390+
expectedExecutor.getIdentity(),
345391
swift_getObjectType(currentExecutor.getIdentity()),
346-
executor.getSerialExecutorWitnessTable());
392+
expectedExecutor.getSerialExecutorWitnessTable());
347393
}
348394

349-
return false;
395+
// This provides a last-resort check by giving the expected SerialExecutor the
396+
// chance to perform a check using some external knowledge if perhaps we are,
397+
// after all, on this executor, but the Swift concurrency runtime was just not
398+
// aware.
399+
//
400+
// Unless handled in `swift_task_checkIsolated` directly, this should call
401+
// through to the executor's `SerialExecutor.checkIsolated`.
402+
//
403+
// This call is expected to CRASH, unless it has some way of proving that
404+
// we're actually indeed running on this executor.
405+
//
406+
// For example, when running outside of Swift concurrency tasks, but trying to
407+
// `MainActor.assumeIsolated` while executing DIRECTLY on the main dispatch
408+
// queue, this allows Dispatch to check for this using its own tracking
409+
// mechanism, and thus allow the assumeIsolated to work correctly, even though
410+
// the code executing is not even running inside a Task.
411+
//
412+
// Note that this only works because the closure in assumeIsolated is
413+
// synchronous, and will not cause suspensions, as that would require the
414+
// presence of a Task.
415+
swift_task_checkIsolated(expectedExecutor);
416+
417+
// The checkIsolated call did not crash, so we are on the right executor.
418+
return true;
350419
}
351420

352421
/// Logging level for unexpected executors:

stdlib/public/Concurrency/CooperativeGlobalExecutor.inc

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,13 @@ static void insertDelayedJob(Job *newJob, JobDeadline deadline) {
136136
*position = newJob;
137137
}
138138

139+
SWIFT_CC(swift)
140+
static void swift_task_checkIsolatedImpl(SerialExecutorRef executor) {
141+
_task_serialExecutor_checkIsolated(
142+
executor.getIdentity(), swift_getObjectType(executor.getIdentity()),
143+
executor.getSerialExecutorWitnessTable());
144+
}
145+
139146
/// Insert a job into the cooperative global queue with a delay.
140147
SWIFT_CC(swift)
141148
static void swift_task_enqueueGlobalWithDelayImpl(JobDelay delay,

stdlib/public/Concurrency/DispatchGlobalExecutor.inc

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
/// swift_task_enqueueGlobalImpl
1919
/// swift_task_enqueueGlobalWithDelayImpl
2020
/// swift_task_enqueueMainExecutorImpl
21+
/// swift_task_checkIsolated
2122
/// as well as any Dispatch-specific functions for the runtime.
2223
///
2324
///===------------------------------------------------------------------===///
@@ -233,7 +234,7 @@ static void swift_task_enqueueGlobalImpl(Job *job) {
233234

234235
SWIFT_CC(swift)
235236
static void swift_task_enqueueGlobalWithDelayImpl(JobDelay delay,
236-
Job *job) {
237+
Job *job) {
237238
assert(job && "no job provided");
238239

239240
dispatch_function_t dispatchFunction = &__swift_run_job;
@@ -383,3 +384,39 @@ void swift::swift_task_enqueueOnDispatchQueue(Job *job,
383384
auto queue = reinterpret_cast<dispatch_queue_t>(_queue);
384385
dispatchEnqueue(queue, job, (dispatch_qos_class_t)priority, queue);
385386
}
387+
388+
/// Recognize if the SerialExecutor is specifically a `DispatchSerialQueue`
389+
/// by comparing witness tables and return it if true.
390+
static dispatch_queue_s *getAsDispatchSerialQueue(SerialExecutorRef executor) {
391+
if (!executor.hasSerialExecutorWitnessTable()) {
392+
return nullptr;
393+
}
394+
395+
auto executorWitnessTable = reinterpret_cast<const WitnessTable *>(
396+
executor.getSerialExecutorWitnessTable());
397+
auto serialQueueWitnessTable = reinterpret_cast<const WitnessTable *>(
398+
_swift_task_getDispatchQueueSerialExecutorWitnessTable());
399+
400+
if (swift_compareWitnessTables(executorWitnessTable,
401+
serialQueueWitnessTable)) {
402+
return reinterpret_cast<dispatch_queue_s *>(executor.getIdentity());
403+
} else {
404+
return nullptr;
405+
}
406+
}
407+
408+
/// If the executor is a `DispatchSerialQueue` we're able to invoke the
409+
/// dispatch's precondition API directly -- this is more efficient than going
410+
/// through the runtime call to end up calling the same API, and also allows us
411+
/// to perform this assertion on earlier platforms, where the `checkIsolated`
412+
/// requirement/witness was not shipping yet.
413+
SWIFT_CC(swift)
414+
static void swift_task_checkIsolatedImpl(SerialExecutorRef executor) {
415+
if (auto queue = getAsDispatchSerialQueue(executor)) {
416+
dispatch_assert_queue(queue);
417+
} else if (executor.hasSerialExecutorWitnessTable()) {
418+
_task_serialExecutor_checkIsolated(
419+
executor.getIdentity(), swift_getObjectType(executor.getIdentity()),
420+
executor.getSerialExecutorWitnessTable());
421+
}
422+
}

stdlib/public/Concurrency/Executor.swift

Lines changed: 63 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,43 @@ public protocol SerialExecutor: Executor {
100100
/// perspective–to execute code assuming one on the other.
101101
@available(SwiftStdlib 5.9, *)
102102
func isSameExclusiveExecutionContext(other: Self) -> Bool
103+
104+
/// Last resort "fallback" isolation check, called when the concurrency runtime
105+
/// is comparing executors e.g. during ``assumeIsolated()`` and is unable to prove
106+
/// serial equivalence between the expected (this object), and the current executor.
107+
///
108+
/// During executor comparison, the Swift concurrency runtime attempts to compare
109+
/// current and expected executors in a few ways (including "complex" equality
110+
/// between executors (see ``isSameExclusiveExecutionContext(other:)``), and if all
111+
/// those checks fail, this method is invoked on the expected executor.
112+
///
113+
/// This method MUST crash if it is unable to prove that the current execution
114+
/// context belongs to this executor. At this point usual executor comparison would
115+
/// have already failed, though the executor may have some external tracking of
116+
/// threads it owns, and may be able to prove isolation nevertheless.
117+
///
118+
/// A default implementation is provided that unconditionally crashes the
119+
/// program, and prevents calling code from proceeding with potentially
120+
/// not thread-safe execution.
121+
///
122+
/// - Warning: This method must crash and halt program execution if unable
123+
/// to prove the isolation of the calling context.
124+
@available(SwiftStdlib 6.0, *)
125+
func checkIsolated()
126+
127+
}
128+
129+
@available(SwiftStdlib 6.0, *)
130+
extension SerialExecutor {
131+
132+
@available(SwiftStdlib 6.0, *)
133+
public func checkIsolated() {
134+
#if !$Embedded
135+
fatalError("Unexpected isolation context, expected to be executing on \(Self.self)")
136+
#else
137+
Builtin.int_trap()
138+
#endif
139+
}
103140
}
104141

105142
/// An executor that may be used as preferred executor by a task.
@@ -120,7 +157,7 @@ public protocol SerialExecutor: Executor {
120157
///
121158
/// Unstructured tasks do not inherit the task executor.
122159
@_unavailableInEmbedded
123-
@available(SwiftStdlib 9999, *)
160+
@available(SwiftStdlib 6.0, *)
124161
public protocol TaskExecutor: Executor {
125162
// This requirement is repeated here as a non-override so that we
126163
// get a redundant witness-table entry for it. This allows us to
@@ -152,7 +189,7 @@ public protocol TaskExecutor: Executor {
152189
}
153190

154191
@_unavailableInEmbedded
155-
@available(SwiftStdlib 9999, *)
192+
@available(SwiftStdlib 6.0, *)
156193
extension TaskExecutor {
157194
public func asUnownedTaskExecutor() -> UnownedTaskExecutor {
158195
UnownedTaskExecutor(ordinary: self)
@@ -270,7 +307,7 @@ public struct UnownedSerialExecutor: Sendable {
270307

271308

272309
@_unavailableInEmbedded
273-
@available(SwiftStdlib 9999, *)
310+
@available(SwiftStdlib 6.0, *)
274311
@frozen
275312
public struct UnownedTaskExecutor: Sendable {
276313
#if $BuiltinExecutor
@@ -279,7 +316,7 @@ public struct UnownedTaskExecutor: Sendable {
279316

280317
/// SPI: Do not use. Cannot be marked @_spi, since we need to use it from Distributed module
281318
/// which needs to reach for this from an @_transparent function which prevents @_spi use.
282-
@available(SwiftStdlib 9999, *)
319+
@available(SwiftStdlib 6.0, *)
283320
public var _executor: Builtin.Executor {
284321
self.executor
285322
}
@@ -303,24 +340,34 @@ public struct UnownedTaskExecutor: Sendable {
303340
}
304341

305342
@_unavailableInEmbedded
306-
@available(SwiftStdlib 9999, *)
343+
@available(SwiftStdlib 6.0, *)
307344
extension UnownedTaskExecutor: Equatable {
308345
@inlinable
309346
public static func == (_ lhs: UnownedTaskExecutor, _ rhs: UnownedTaskExecutor) -> Bool {
310347
unsafeBitCast(lhs.executor, to: (Int, Int).self) == unsafeBitCast(rhs.executor, to: (Int, Int).self)
311348
}
312349
}
313350

314-
/// Checks if the current task is running on the expected executor.
351+
/// Returns either `true` or will CRASH if called from a different executor
352+
/// than the passed `executor`.
353+
///
354+
/// This method will attempt to verify the current executor against `executor`,
355+
/// and as a last-resort call through to `SerialExecutor.checkIsolated`.
356+
///
357+
/// This method will never return `false`. It either can verify we're on the
358+
/// correct executor, or will crash the program. It should be used in
359+
/// isolation correctness guaranteeing APIs.
315360
///
316361
/// Generally, Swift programs should be constructed such that it is statically
317362
/// known that a specific executor is used, for example by using global actors or
318363
/// custom executors. However, in some APIs it may be useful to provide an
319364
/// additional runtime check for this, especially when moving towards Swift
320365
/// concurrency from other runtimes which frequently use such assertions.
366+
///
321367
/// - Parameter executor: The expected executor.
368+
@_spi(ConcurrencyExecutors)
322369
@available(SwiftStdlib 5.9, *)
323-
@_silgen_name("swift_task_isOnExecutor")
370+
@_silgen_name("swift_task_isOnExecutor") // This function will CRASH rather than return `false`!
324371
public func _taskIsOnExecutor<Executor: SerialExecutor>(_ executor: Executor) -> Bool
325372

326373
@_spi(ConcurrencyExecutors)
@@ -361,6 +408,13 @@ internal func _task_serialExecutor_isSameExclusiveExecutionContext<E>(current cu
361408
currentExecutor.isSameExclusiveExecutionContext(other: executor)
362409
}
363410

411+
@available(SwiftStdlib 6.0, *)
412+
@_silgen_name("_task_serialExecutor_checkIsolated")
413+
internal func _task_serialExecutor_checkIsolated<E>(executor: E)
414+
where E: SerialExecutor {
415+
executor.checkIsolated()
416+
}
417+
364418
/// Obtain the executor ref by calling the executor's `asUnownedSerialExecutor()`.
365419
/// The obtained executor ref will have all the user-defined flags set on the executor.
366420
@available(SwiftStdlib 5.9, *)
@@ -373,7 +427,7 @@ internal func _task_serialExecutor_getExecutorRef<E>(_ executor: E) -> Builtin.E
373427
/// Obtain the executor ref by calling the executor's `asUnownedTaskExecutor()`.
374428
/// The obtained executor ref will have all the user-defined flags set on the executor.
375429
@_unavailableInEmbedded
376-
@available(SwiftStdlib 9999, *)
430+
@available(SwiftStdlib 6.0, *)
377431
@_silgen_name("_task_executor_getTaskExecutorRef")
378432
internal func _task_executor_getTaskExecutorRef(_ taskExecutor: any TaskExecutor) -> Builtin.Executor {
379433
return taskExecutor.asUnownedTaskExecutor().executor
@@ -396,7 +450,7 @@ where E: SerialExecutor {
396450
}
397451

398452
@_unavailableInEmbedded
399-
@available(SwiftStdlib 9999, *)
453+
@available(SwiftStdlib 6.0, *)
400454
@_silgen_name("_swift_task_enqueueOnTaskExecutor")
401455
internal func _enqueueOnTaskExecutor<E>(job unownedJob: UnownedJob, executor: E) where E: TaskExecutor {
402456
#if !SWIFT_STDLIB_TASK_TO_THREAD_MODEL_CONCURRENCY

0 commit comments

Comments
 (0)