Hello everyone,
We'd like to share a proposal that've been working towards for a while now.
Read the full proposal here: SE-NNNN: Task Executor Preference.
(In case of small typos, please comment on the pull request)
The proposal introduces task executors which enable a structured task hierarchy to have a "preference" where all of its tasks should execute.
In short, task executors allow setting an executor that will be used by the task. The task is enqueued on ("starts on") that executor, and attempts to run on it whenever possible. This includes running nonisolated async functions on the preferred executor rather the global one, and even running default actors on it.
The execution semantics of asynchronous code are as follows below:
Currently the decision where an async
function or closure is going to execute is binary:
// `func` execution semantics before this proposal
[ func / closure ] - /* where should it execute? */
|
+--------------+ +==========================+
+- no - | is isolated? | - yes -> | default (actor) executor |
| +--------------+ +==========================+
|
| +==========================+
+-------------------------------> | on global conc. executor |
+==========================+
This proposal introduces a way to control hopping off to the global concurrent pool for nonisolated
functions and closures. This is expressed as task executor preference and is sticky to the task and entire structured task hierarchy created from a task with a specified preference. This changes the current decision diagram to the following:
// `func` execution semantics with this proposal
[ func / closure ] - /* where should it execute? */
|
+--------------+ +===========================+
+-------- | is isolated? | - yes -> | actor has unownedExecutor |
| +--------------+ +===========================+
| | |
| yes no
| | |
| v v
| +=======================+ /* task executor preference? */
| | on specified executor | | |
| +=======================+ yes no
| | |
| | v
| | +==========================+
| | | default (actor) executor |
| v +==========================+
v +==============================+
/* task executor preference? */ ---- yes ----> | on Task's preferred executor |
| +==============================+
no
|
v
+===============================+
| on global concurrent executor |
+===============================+
This allows specialized applications to optimize context-switching, by e.g. using an event loop (e.g. from NIO), as the task's executor. Tasks using such executor minimize context switching and can yield better performance in such very specific applications. This can also be used to isolate blocking IO tasks to specific executors dedicated to such work.
It should be noted that one should have a deeper understanding of context switching, and blocking in your application before attempting to use task executors to address them, as they can also cause negative effects -- e.g. if an actor is forced to change task executors consitently back and forth, while normally it could keep draining its queue more efficiently without changing executors.
For more background, you may want to read:
- SE-0338: Clarify the Execution of Non-Actor-Isolated Async Functions which defined that
nonisolated
async functions to always execute on the global pool, rather than dangerously hanging onto the calling actor's executor. - SE-0392: Custom Actor Executors which was the first steps towards customizing swift's concurrency runtime semantics by providing custom executors for actors.
Implementation of this proposal is still in progress, though we'll share when it will be ready to give it a spin.