Swift Macros / Property Wrappers to help use DispatchQueue correctly?

Hi guys,

My existing concurrent code uses a lot of traditional DispatchQueue patterns, from using a serial dispatch queue to protect internal state to callbacks to user specified callback queues.

I am trying to bring that code into the new Swift concurrency world, and as much as I love the experience, one thing becomes clear, at least for me. It's impossible right now (due to many isolation related shortcomings that are being worked on) to move everything to actors just not yet.

And although I use a lot of .dispatchPrecondition and other helper methods to use dispatch queues cleverly, ...

... it got me thinking. What if I create a small package that will vend several primitives, property wrappers, or macros to help using DispatchQueue based concurrency systems correctly?

The way I think about it (before writing any code):

IdentifiableDispatchQueue will represent a triple with DispatchQueue, DispatchSpecificKey, and associated DispatchSpecificValue.

Than when declaring a variable, I can annotate it with either a property wrapper, or Swift macro to make sure it is only touched on the queue, for example like this:

@OnQueue(myIdentifiableSerialQueue, ordering: .strict)
var someInternalState: String = "Hello World"

Now either the property wrapper, or the Swift macro will make sure that

  • all reads will happen either directly when already on queue, or via DispatchQueue.sync
  • all writes will happen via DispatchQueue.async call when ordering: .strict, or directly when we are already on queue, and ordering: .relaxed.

Determining whether we are on queue will be done vie DispatchQueue.getSpecific to match against the IdentifiableDispatchQueue instance that is specifed in var declaration.

Now, I do not see any drawbacks right now (apart from potential perf impact) when using the Macro or PropertyWrapper on instance variables.

I think I can come up with a clever way to provide also .sync and .async methods on IdentifiableDispatchQueue to execute piece of work directly, or via .sync, or .async call respecting work ordering in case of .async work.

What do you think? Do you face similar problems? Are there any existing swift packages doing similar? Is it a good idea?

As much as I tried to find something, I have failed...

thanks for any inputs,
Martin

For what it's worth this is sticking to queues while we do "the same" with global actors... the way swift concurrency works and isolation checking etc, you won't get much (any) safety checking benefits from an approach like this and may eventually feel like you're fighting isolation checking -- because it does not work on queue semantics, but on actors, so you may "know" that all some things are on the same queue but the compiler won't be able to know -- since it wasn't expressed using isolation (actors, tasks).

Overall not an approach I'd personally pursue, as it feels like in simple oneliners might be fine, but won't scale to a wide application. Swift's concurrency model is designed for that on the other hand :thinking:

Just my take on it, you can of course make your own packages and approaches to things :slight_smile:

3 Likes