How to use swift concurrency from a serial queue?

I have existing code:

// A
serialQueue.async { task1() }
// B
serialQueue.async { task2() }
// C
serialQueue.async { task3() }

But I changed task2 to be async, and ended up changing the call as well:

// B
Task { await task2() }

However, I think there is now a data race, because task2 is not confined to serialQueue. How do I bridge the gap here?

By "confined" here, are you looking to:

  1. Ensure that task2 just runs on serialQueue's thread so that data synchronization is maintained,
  2. Maintain the serial behavior of your existing code such that task3 doesn't start executing until task2 completes, or
  3. Something else?

I assume (2), but this is non-trivial to achieve:

  1. The "simple" solution would be to effectively have your B callsite block the thread synchronously until task2() completes, which can be extremely unsafe and prone to deadlocking, depending on what task2 does
  2. You could write a custom executor that wraps your serialQueue, write an actor that uses that executor as its serial executor, then submit task2 to that actor but: under the hood, Tasks are split up into the individual Jobs that represent the synchronous sections between await calls inside of a task, and those are the work submitted to an executor. That means that task2 would get submitted to your queue in chunks, and there'd be no good way of preventing task3 of interleaving with task2 (achieving (1) but not (2) above)
  3. If you have the option, you could investigate switching over from a serial queue to an AsyncStream, which would allow you to submit tasks sequentially and produce results in order — but this might not be possible if you don't own serialQueue or have to continue using queues for some other reason

There might be more approaches, but it's hard to say specifically without knowing more about your use-case, and the work items you're trying to run.

1 Like