The `async` operation is a global function that initiates asynchronous
work on behalf of the synchronous code that calls it. Unlike `detach`,
`async` inherits priority, actor context, and other aspects of the
synchronous code that initiates it, making it a better "default"
operation for creating asynchronous work than `detach`. The `detach`
operation is still important for creating truly detached tasks that
can later be `await`'d or cancelled if needed.
Implements the main entry point for rdar://76927008.
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.
For ordinary memory-management reasons, this should only ever
happen when there will be no more uses of the actor outside of the
actor runtime. The actor runtime, meanwhile, doesn't care about
anything except the default-actor control state of the actor. So
we can just allow the rest of the actor to be destructed when it
isn't needed anymore, then destroy the actor state and deallocate
the object when we get around to switching off the executor.
This does assume that the task doesn't do anything which semantically
detects the executor it's on before switching off it, since doing so
might read a bogus executor. However, we should only get an executor
in a zombie state like this when a hop has been removed or reordered,
and detection events should count as inhibiting that and forcing the
true executor to be switched to (and thus detected).
(But maybe lifetime optimization can make this happen? Maybe we
need semantic detection to filter out zombie executors.)
Previously, if this happened, we simply left the actor in a running
state, causing any further jobs submitted to it to never be executed.
I can only speculate why this wasn't showing up in testing.
Also, change swift_job_run so that it prevents switching if the executor
passed in is not generic. This is an entrypoint for arbitrary executors
and generally should not allow unexpected switching (if someday custom
executors participate in that scheme). This infrastructure will also
be useful for implementing the `async let` semantics of running
synchronously until the task reaches a suspension point.
Finally, improve the #if'ed logging code throughout the task/actor runtime.
Take the existing CompatibilityOverride mechanism and generalize it so it can be used in both the runtime and Concurrency libraries. The mechanism is preprocessor-heavy, so this requires some tricks. Use the SWIFT_TARGET_LIBRARY_NAME define to distinguish the libraries, and use a different .def file and mach-o section name accordingly.
We want the global/main executor functions to be a little more flexible. Instead of using the override mechanism, we expose function pointers that can be set by the compatibility library, or by any other code that wants to use a custom implementation.
rdar://73726764
Tasks shouldn't normally hog the actor context indefinitely after making a call that's bound to
that actor, since that prevents the actor from potentially taking on other jobs it needs to
be able to address. Set up SILGen so that it saves the current executor (using a new runtime
entry point) and hops back to it after every actor call, not only ones where the caller context
is also actor-bound.
The added executor hopping here also exposed a bug in the runtime implementation while processing
DefaultActor jobs, where if an actor job returned to the processing loop having already yielded
the thread back to a generic executor, we would still attempt to make the actor give up the thread
again, corrupting its state.
rdar://71905765
Most of the async runtime functions have been changed to not
expect the task and executor to be passed in. When knowing the
task and executor is necessary, there are runtime functions
available to recover them.
The biggest change I had to make to a runtime function signature
was to swift_task_switch, which has been altered to expect to be
passed the context and resumption function instead of requiring
the caller to park the task. This has the pleasant consequence
of allowing the implementation to very quickly turn around when
it recognizes that the current executor is satisfactory. It does
mean that on arm64e we have to sign the continuation function
pointer as an argument and then potentially resign it when
assigning into the task's resume slot.
rdar://70546948
I have identified the following conceptual synchronization points at
which task data and computation can cross thread boundaries. We need to
model these in TSan to avoid false positives:
Awaiting an async task (`AsyncTask::waitFuture`), which has two cases:
1) The task has completed (`AsyncTask::completeFuture`). Everything
that happened during task execution "happened before" before the
point where we access its result. We synchronize on the *awaited*
task.
2) The task is still executing: the current execution is suspended and
the waiting task is put into the list of "waiters". Once the awaited
task completes, the waiters will be scheduled. In this case, we
synchronize on the *waiting* task.
Note: there is a similar relationship for task groups which I still have
to investigate. I will follow-up with an additional patch and tests.
Actor job execution (`swift::runJobInExecutorContext`):
Job scheduling (`swift::swift_task_enqueue`) precedes/happens before job
execution. Also all job executions (switching actors or suspend/resume)
are serially ordered.
Note: the happens-before edge for schedule->execute isn't strictly
needed in most cases since scheduling calls through to libdispatch's
`dispatch_async_f`, which we already intercept and model in TSan.
However, I am trying to model Swift Task semantics to increase the
chance of things to continue to work in case the "task backend" is
switched out.
rdar://74256733
This is conditional on UseAsyncLowering and in the future should also be
conditional on `clangTargetInfo.isSwiftAsyncCCSupported()` once that
support is merged.
Update tests to work either with swiftcc or swifttailcc.
We expect to iterate on this quite a bit, both publicly
and internally, but this is a fine starting-point.
I've renamed runAsync to runAsyncAndBlock to underline
very clearly what it does and why it's not long for this
world. I've also had to give it a radically different
implementation in an effort to make it continue to work
given an actor implementation that is no longer just
running all work synchronously.
The major remaining bit of actor-scheduling work is to
make swift_task_enqueue actually do something sensible
based on the executor it's been given; currently it's
expecting a flag that IRGen simply doesn't know to set.