Skip to content

Commit ded2187

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. add swift_task_checkIsolatedImpl to CooperativeGlobalExecutor
1 parent 16d3997 commit ded2187

17 files changed

+550
-64
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: 83 additions & 12 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,109 @@ 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(concurrency): consider removing this special case, as checkIsolated will compare against mainQueue already
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

342-
return _task_serialExecutor_isSameExclusiveExecutionContext(
388+
auto result = _task_serialExecutor_isSameExclusiveExecutionContext(
343389
currentExecutor.getIdentity(),
344-
executor.getIdentity(),
390+
expectedExecutor.getIdentity(),
345391
swift_getObjectType(currentExecutor.getIdentity()),
346-
executor.getSerialExecutorWitnessTable());
392+
expectedExecutor.getSerialExecutorWitnessTable());
393+
394+
return result;
347395
}
348396

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

352423
/// Logging level for unexpected executors:

stdlib/public/Concurrency/CooperativeGlobalExecutor.inc

Lines changed: 8 additions & 0 deletions
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_checkIsolatedImpl
2122
/// as well as any cooperative-executor-specific functions in the runtime.
2223
///
2324
///===------------------------------------------------------------------===///
@@ -136,6 +137,13 @@ static void insertDelayedJob(Job *newJob, JobDeadline deadline) {
136137
*position = newJob;
137138
}
138139

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

stdlib/public/Concurrency/DispatchGlobalExecutor.inc

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,13 @@
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
///===------------------------------------------------------------------===///
2425

2526
#if SWIFT_CONCURRENCY_ENABLE_DISPATCH
27+
#include "swift/Runtime/HeapObject.h"
2628
#include <dispatch/dispatch.h>
2729
#if defined(_WIN32)
2830
#include <Windows.h>
@@ -233,7 +235,7 @@ static void swift_task_enqueueGlobalImpl(Job *job) {
233235

234236
SWIFT_CC(swift)
235237
static void swift_task_enqueueGlobalWithDelayImpl(JobDelay delay,
236-
Job *job) {
238+
Job *job) {
237239
assert(job && "no job provided");
238240

239241
dispatch_function_t dispatchFunction = &__swift_run_job;
@@ -383,3 +385,56 @@ void swift::swift_task_enqueueOnDispatchQueue(Job *job,
383385
auto queue = reinterpret_cast<dispatch_queue_t>(_queue);
384386
dispatchEnqueue(queue, job, (dispatch_qos_class_t)priority, queue);
385387
}
388+
389+
/// Recognize if the SerialExecutor is specifically a `DispatchSerialQueue`
390+
/// by comparing witness tables and return it if true.
391+
static dispatch_queue_s *getAsDispatchSerialQueue(SerialExecutorRef executor) {
392+
if (!executor.hasSerialExecutorWitnessTable()) {
393+
return nullptr;
394+
}
395+
396+
auto executorWitnessTable = reinterpret_cast<const WitnessTable *>(
397+
executor.getSerialExecutorWitnessTable());
398+
auto serialQueueWitnessTable = reinterpret_cast<const WitnessTable *>(
399+
_swift_task_getDispatchQueueSerialExecutorWitnessTable());
400+
401+
if (swift_compareWitnessTables(executorWitnessTable,
402+
serialQueueWitnessTable)) {
403+
return reinterpret_cast<dispatch_queue_s *>(executor.getIdentity());
404+
} else {
405+
return nullptr;
406+
}
407+
}
408+
409+
/// If the executor is a `DispatchSerialQueue` we're able to invoke the
410+
/// dispatch's precondition API directly -- this is more efficient than going
411+
/// through the runtime call to end up calling the same API, and also allows us
412+
/// to perform this assertion on earlier platforms, where the `checkIsolated`
413+
/// requirement/witness was not shipping yet.
414+
SWIFT_CC(swift)
415+
static void swift_task_checkIsolatedImpl(SerialExecutorRef executor) {
416+
// If it is the main executor, compare with the Main queue
417+
if (executor.isMainExecutor()) {
418+
dispatch_assert_queue(dispatch_get_main_queue());
419+
return;
420+
}
421+
422+
// if able to, use the checkIsolated implementation in Swift
423+
if (executor.hasSerialExecutorWitnessTable()) {
424+
_task_serialExecutor_checkIsolated(
425+
executor.getIdentity(), swift_getObjectType(executor.getIdentity()),
426+
executor.getSerialExecutorWitnessTable());
427+
return;
428+
}
429+
430+
if (auto queue = getAsDispatchSerialQueue(executor)) {
431+
// if the executor was not SerialExecutor for some reason but we're able
432+
// to get a queue from it anyway, use the assert directly on it.
433+
dispatch_assert_queue(queue); // TODO(concurrency): could we report a better message here somehow?
434+
return;
435+
}
436+
437+
// otherwise, we have no way to check, so report an error
438+
// TODO: can we swift_getTypeName(swift_getObjectType(executor.getIdentity()), false).data safely in the message here?
439+
swift_Concurrency_fatalError(0, "Incorrect actor executor assumption");
440+
}

0 commit comments

Comments
 (0)