Support custom executors in Swift concurrency

How does UnownedJobRef relate to PartialTask from the Structured Concurrency proposal? Are they the same?

WRT backpressure - is it possible for an executor to cancel a partial task/job?

Let’s say I’m writing a game engine and I enqueue my render tasks to a specific executor - in the event of a backlog, and since it is not allowed to fail to execute any of those tasks, can it communicate that the task should abort ASAP, since it knows another render task will be coming?

Also, I believe it should be safe for an executor to access the task-local storage of any tasks that are enqueued on it, is that correct? Since it should be able to, on the same underlying thread, access the same local storage that the task it executes can access, and it is free to execute any enqueued task whenever it likes.

Sorry, I lost track of this.

Yes, I intend this as a replacement name.

Hmm. It's technically possible, and I suppose if you knew something about the specific task it might be fine. If you don't have that knowledge, it might be better to trigger some sort of back-pressure notification to slow the creation of new work; I don't know what that looks like in terms of API right now.

Yes, I think as long as the task isn't actively running, this is fine.

1 Like

Sorry, but which one is the new name? PartialAsyncTask? :sweat_smile:

That's great. I don't want to push you on any kind of API design, but if an executor can read from Task-local storage, surely that could be used for some kind of Task-specific metadata to aid in scheduling.

Looking at the current implementation, it seems that Task-local storage is implemented as a linked-list - so it seems to me that if it's safe for an executor to read, it might also be possible for it to push new values to implement some kind of backpressure signal.

This pitch, the structured concurrent proposal, and others, frequently mention the importance of custom executors to achieve high performance and to allow the language concurrency design to map to a wide variety of platforms. I don't tend to write much of that kind of code myself, but I have friends who work in the gaming industry, and I know from them that having fine-grained control of scheduling can be critical. The built-in priority system is not going to be nearly enough for those kinds of use-cases.

It seems to me that a flexible scheduling solution will need to have some kind of unique Task metadata - some tag allows you to attach application-specific information for the benefit of particular executors (e.g. this PartialAsyncTask is part of a calculation from the physics engine, or is updating shadows or reflections), and that Task-local storage could work for that.

I would like to talk about "jobs" rather than "partial tasks".

3 Likes

Hello @John_McCall

I am trying my best to understand this pitch but I admit it is still a bit hard to grasp.
So my question is, are executors the correct place to introduce enqueuing jobs after a delay or at a specific time? I asked a question in a different thread about how to replicate asyncAfter using dispatch queues but since there is no specific queue to "address" on an actor are executors taking some of that repressibility and allowing for such functionality, @ktoso directed me to this pitch (I might have misinterpreted it though)?

Thanks,
Filip

Hi there,
There’s two parts to your question I guess.

“run something after 2 seconds” is easily expressed with a task that just sleeps until then:

Task { 
  await Task.sleep(…)
  await runThing()
}

The second part of the question I think I’m not remembering the context for and you have not re-stated it here (?).

Generally you get isolated execution by simply running code on an actor. You don’t really need to execute on a specific queue to get this.

If this is about executing some e.g. blocking operation on a specific queue in order to isolate it from the global cooperative pool, then yes indeed that’s what would be nicely enabled by these custom executors — you can then dedicate one executor for “nasty blocking work” and execute all such work on it, without harming and resource starving any of the other actors.

Is that what you’re looking for or am I misremembering the context of your question?

1 Like

In the absence of deadline-based scheduling, this easy example won’t work correctly under contention.

1 Like

Hey @ktoso

The example you posted clarifies things for me, as I thought I miss-understood that is part of the Executor.

I know this goes into Apple frameworks question, but how would you reference an Actor's context for APIs from combine such as throttle?

https://developer.apple.com/documentation/combine/publishers/throttle

Thanks,
Filip

It’s not clear yet how executors relate to combine schedulers.

But the question of “how to get an actors executor” is alluded to in this pitch here — it’s likely (pending evolution discussion ofc) going to be a property on every actor.

4 Likes

Is this pitch likely to evolve into a full proposal in time to be included as part of swift 5.5? As I understand it, custom executors are key to achieving optimal performance for certain libraries, such as SwiftNIO and it would be awesome to adopt Swift’s new concurrency proposals on the server with the release of swift 5.5 without compromising performance!

Hi Aaron,
there's a lot of various work to be done for the server story and we are working on those.

The custom executors are unlikely to be a 5.5 feature, it is too late for that.

Having that said, I don't think this should prevent most server systems from adopting swift concurrency. It is only the absolute most high performance use cases like proxies or similar which might see this as an issue. I don't think that it'll be a problem in normal web applications and API endpoints.

Having that said, yes, the custom executors will offer possibility to more precisely fine-tune performance of server applications by avoiding event loop hops etc. "Hops" between actors are much cheaper than hops between event loops already by the way as far as I've observed so far at least.


As usual: always measure and then make decisions. We'd very much welcome reports from real apps adopting the feature so we can feed it back into feature and performance work.

The best you can do right now is to use the feature and report back with reproducers, performance findings and concerns from real apps that we can reproduce and then polish the runtime to work better there! Thanks in advance.

6 Likes

Thanks for the prompt reply! I had come to think that the performance penalty would be more significant, so I am glad to hear that quick adopters in “lower stakes” scenarios shouldn’t need to worry too much. Thanks again!

1 Like

I couldn't quite picture what this section is describing:

The default serial executor implementation is separately instantiated for each actor that doesn't declare a custom executor; see "Actor executors" below. It is based on an "asynchronous lock" design which allows existing threads to immediately begin executing code on behalf of the actor rather than requiring functions to suspend and resume executing asynchronously. This process is called switching.

I wonder if custom executors will intersect with testing? Say, for example, I wanted to write a unit test for an async stream that emits values a, b after deadline t1 and values c, d after t2 has passed. Would I use custom executors to control the passing of time and assert expected values at those points in "time" without the test literally taking that time?

An executor is an object to which opaque jobs can be submitted to be run later.

I suspect later can mean more than just the time it takes back-to-back work to be drained from a queue. It leaves open the possibility of a deadline for both serial and concurrent queues. This makes me wonder: is Task scheduling/deadlines something executors or tasks themselves should be responsible for?

Fumbling around in the dark, it looks like Task.sleep() is implemented as a convenience API around a global executor. Can I write a unit test for some code involving Task.sleep() without the test literally taking that time?

So, for example, I have a screen that wants to enable the "next" button for a handful of seconds.

@MainActor final class TermsAndConditionsViewModel: ObservableObject {
   @Published var isNextButtonEnabled = false

   func didShowTermsAndConditions() {
     async {
         await Task.sleep(4 * 1e9)
         self.isNextButtonEnabled = true
      }
   }
}

How could I write a unit test for this without the test taking four seconds? On Apple platforms, I could pass a Combine.Scheduler into this instance and have test cases use a test or immediate scheduler and non-test use cases use DispatchQueue.main.

3 Likes