[Concurrency] Introduce runtime detection of data races.

Through various means, it is possible for a synchronous actor-isolated
function to escape to another concurrency domain and be called from
outside the actor. The problem existed previously, but has become far
easier to trigger now that `@escaping` closures and local functions
can be actor-isolated.

Introduce runtime detection of such data races, where a synchronous
actor-isolated function ends up being called from the wrong executor.
Do this by emitting an executor check in actor-isolated synchronous
functions, where we query the executor in thread-local storage and
ensure that it is what we expect. If it isn't, the runtime complains.
The runtime's complaints can be controlled with the environment
variable `SWIFT_UNEXPECTED_EXECUTOR_LOG_LEVEL`:

  0 - disable checking
  1 - warn when a data race is detected
  2 - error and abort when a data race is detected

At an implementation level, this introduces a new concurrency runtime
entry point `_checkExpectedExecutor` that checks the given executor
(on which the function should always have been called) against the
executor on which is called (which is in thread-local storage). There
is a special carve-out here for `@MainActor` code, where we check
against the OS's notion of "main thread" as well, so that `@MainActor`
code can be called via (e.g.) the Dispatch library's
`DispatchQueue.main.async`.

The new SIL instruction `extract_executor` performs the lowering of an
actor down to its executor, which is implicit in the `hop_to_executor`
instruction. Extend the LowerHopToExecutor pass to perform said
lowering.
This commit is contained in:
Doug Gregor
2021-04-12 14:32:02 -07:00
parent ceeee459b4
commit e77a27e8ed
30 changed files with 466 additions and 44 deletions

View File

@@ -462,9 +462,6 @@ void SILGenFunction::emitProlog(CaptureInfo captureInfo,
}
}
if (!F.isAsync())
return;
// Initialize ExpectedExecutor if the function is an actor-isolated
// function or closure.
if (auto *funcDecl =
@@ -478,9 +475,15 @@ void SILGenFunction::emitProlog(CaptureInfo captureInfo,
case ActorIsolation::ActorInstance: {
assert(selfParam && "no self parameter for ActorInstance isolation");
auto loc = RegularLocation::getAutoGeneratedLocation(F.getLocation());
ManagedValue selfArg = ManagedValue::forUnmanaged(F.getSelfArgument());
ExpectedExecutor = emitLoadActorExecutor(loc, selfArg);
// Only produce an executor for actor-isolated functions that are async
// or are local functions. The former require a hop, while the latter
// are prone to dynamic data races in code that does not enforce Sendable
// completely.
if (F.isAsync() || funcDecl->isLocalCapture()) {
auto loc = RegularLocation::getAutoGeneratedLocation(F.getLocation());
ManagedValue selfArg = ManagedValue::forUnmanaged(F.getSelfArgument());
ExpectedExecutor = emitLoadActorExecutor(loc, selfArg);
}
break;
}
@@ -514,14 +517,24 @@ void SILGenFunction::emitProlog(CaptureInfo captureInfo,
}
// Jump to the expected executor.
if (ExpectedExecutor)
B.createHopToExecutor(
if (ExpectedExecutor) {
if (F.isAsync()) {
// For an async function, hop to the executor.
B.createHopToExecutor(
RegularLocation::getAutoGeneratedLocation(F.getLocation()),
ExpectedExecutor);
} else {
// For a synchronous function, check that we're on the same executor.
// Note: if we "know" that the code is completely Sendable-safe, this
// is unnecessary. The type checker will need to make this determination.
emitPreconditionCheckExpectedExecutor(
RegularLocation::getAutoGeneratedLocation(F.getLocation()),
ExpectedExecutor);
}
}
}
SILValue SILGenFunction::emitLoadGlobalActorExecutor(Type globalActor) {
assert(F.isAsync());
CanType actorType = CanType(globalActor);
NominalTypeDecl *nominal = actorType->getNominalOrBoundGenericNominal();
VarDecl *sharedInstanceDecl = nominal->getGlobalActorInstance();
@@ -568,29 +581,7 @@ ExecutorBreadcrumb SILGenFunction::emitHopToTargetActor(SILLocation loc,
if (!maybeIso)
return ExecutorBreadcrumb();
auto actorIso = maybeIso.getValue();
Optional<SILValue> executor = None;
switch (actorIso.getKind()) {
case ActorIsolation::Unspecified:
case ActorIsolation::Independent:
break;
case ActorIsolation::ActorInstance: {
// "self" here means the actor instance's "self" value.
assert(maybeSelf.hasValue() && "actor-instance but no self provided?");
auto self = maybeSelf.getValue();
executor = emitLoadActorExecutor(loc, self);
break;
}
case ActorIsolation::GlobalActor:
case ActorIsolation::GlobalActorUnsafe:
executor = emitLoadGlobalActorExecutor(actorIso.getGlobalActor());
break;
}
if (executor) {
if (auto executor = emitExecutor(loc, *maybeIso, maybeSelf)) {
// Record the previous executor to hop back to when we no longer need to
// be isolated to the target actor.
//
@@ -605,6 +596,50 @@ ExecutorBreadcrumb SILGenFunction::emitHopToTargetActor(SILLocation loc,
}
}
Optional<SILValue> SILGenFunction::emitExecutor(
SILLocation loc, ActorIsolation isolation,
Optional<ManagedValue> maybeSelf) {
switch (isolation.getKind()) {
case ActorIsolation::Unspecified:
case ActorIsolation::Independent:
return None;
case ActorIsolation::ActorInstance: {
// "self" here means the actor instance's "self" value.
assert(maybeSelf.hasValue() && "actor-instance but no self provided?");
auto self = maybeSelf.getValue();
return emitLoadActorExecutor(loc, self);
}
case ActorIsolation::GlobalActor:
case ActorIsolation::GlobalActorUnsafe:
return emitLoadGlobalActorExecutor(isolation.getGlobalActor());
}
}
void SILGenFunction::emitPreconditionCheckExpectedExecutor(
SILLocation loc, SILValue executorOrActor) {
auto checkExecutor = SGM.getCheckExpectedExecutor();
if (!checkExecutor)
return;
// Get the executor.
SILValue executor = B.createExtractExecutor(loc, executorOrActor);
// Call the library function that performs the checking.
auto args = emitSourceLocationArgs(loc.getSourceLoc(), loc);
emitApplyOfLibraryIntrinsic(loc, checkExecutor, SubstitutionMap(),
{
args.filenameStartPointer,
args.filenameLength,
args.filenameIsAscii,
args.line,
ManagedValue::forUnmanaged(executor)
},
SGFContext());
}
void ExecutorBreadcrumb::emit(SILGenFunction &SGF, SILLocation loc) {
if (Executor)
SGF.B.createHopToExecutor(loc, Executor);