Task: Is order of task execution deterministic?

I have some code that helps with awaiting emitted values from a Publisher. The glue code uses an actor to synchronize state.

I have some code that looks like this:

        ...
        subscription = publisher
            .first()
            .sink { [weak self] completion in
                guard let self = self else { return }
                Task { await self.handle(completion: completion) }
            } receiveValue: { [weak self] value in
                guard let self = self else { return }
                Task { await self.handle(received: value) }
            }

where self is an actor, so the handle calls must be called from a concurrent context.

What I'm finding is a subtle bug that happens about 5-10% of the time on a colleague's machine where the Task order of execution is not deterministic. The receiveValue block is always called first, and the completion second. But about 5-10% the call to self.handle(completion:) is called before the call to self.handle(received:).

My question is, is this by design or a bug? I cannot repro on my machine. My machine has an M1, his is a recent 16" MBP. We are both using Xcode 13 beta 5.

4 Likes

This is, sadly, as expected.

The problem can be illustrated by the following:

  • create task 1
  • create task 2
  • task 2 runs / PERHAPS even concurrently to: task 1 runs
  • task 2 invokes handle(received:)
    • task 2: just happens to be slightly quicker to get to the invocation
  • task 1 invokes handle(completion:)

There is zero ordering guarantees between “invoke handle(received:)” and “invoke(handle:completion)” because the moment we hit the Task{} we left all scheduling to completely independent tasks with no relationship to eachother at all.

—

I feel a lot of code will be “bitten” by this and—personally—would love for the introduction of something akin to “send” i.e. enqueue the invocation on the actor and then return — thanks to such operation (sometimes called “tell” in other actor runtimes), we’d be able to guarantee ordering in such uni-directional calls.

We can’t express this today.

We either can await for the entire call to complete, or we have to Task{} and cause concurrent execution. The actor has no idea that it will have any calls until those concurrent tasks eventually run and invoke things on it.

A “tell”/“send” operation would operate differently: Instead of Task { await self.handle(completion: completion) } we’d say send self.handle(completion: completion). which would mean “don’t wait for the response but return once the invocation has been enqueued in the actor’s mailbox, this way there is no additional indeterminism injected into the order by the addition of those Tasks that we only created because we don’t want to wait for the results really.

It’s up in the air if we’ll get such operations though — we definitely have hit this issue multiple times already thought and I (personally) really do think it’d be useful to allow such thing.

—

The entire issue can already be seen in a simplified case, like this:

async let a = actor.tell(“A”)
async let b = actor.tell(“B”)
// but we actually meant
// don’t wait actor.tell(“A”)
// don’t wait actor tell(“B”)

It is not deterministic what order the actor gets those messages in, and we have no way to express “enqueue but don’t wait”, so the only way to get the ordering we want to so await on both calls in the current task. The actor is sequential, sure, but the async lets create new tasks and those are executing concurrently, so the actor will get the invocations enqueued in any order.

16 Likes

I can see now reading your post that this is by design.

I heartily agree that this would be a welcome addition to actors. I guess developers could roll their own maybe, but I'm sure quite a few developers would have a hard time getting this right. What is even more insidious about this problem is that most people may not even be aware this pattern is a source of bug with actors; because in my case the bug never manifested on my machine, only on a colleague's machine. I tested it thousands of times. He could get it to reproduce about 1/10 times.

Having and documenting a solution to it, would then make them aware of the problem, and the solution would be put to use. It's a bit like how swift makes you use self within closures, that clues you in as to what has been captured. Having a send/enqueue/tell function on the actor would clue you in that there is a reason for it.

For now, I've found a different solution to our particular bug. But again I agree, I really think it would be useful.

2 Likes

I guess you could create another actor to operate a queue for the first actor?

Then you could say something like

let actorQueue = ActorQueue(self)
:
await actorQueue.send { await self.handle(completion: completion) }

and the ActorQueue would simply run all such enqueued blocks sequentially.

1 Like

How would you send a completion block to the ActorQueue without await?

I think that's the tricky bit. It would have to be a call to a nonisolated actor method, and that would need to somehow enqueue the work.

Is there a workaround for this? I tried out all the new async features in a sample App and I also stumbled across this problem in the context of UI code triggering async methods. Basically the order of the user input is not deterministic anymore when it comes to async work - which is a problem. Actually, I think this is a missing feature that we need - or do I miss something?

As a workaround, i scheduled user input as messages to an AsyncStream and where reading them out in an async loop to get back to correct order - but this introduces new problems (like loosing the information when an async action is over).

Exactly - same problem again

I'm not sure what you mean. I do have an await before send().

Something like this?

actor ActorQueue<TargetActor: Actor> {
    typealias Work = @Sendable (TargetActor) async -> Void
    let actor: TargetActor
    var queue: [Work] = []
    var isProcessing = false
    init(_ actor: TargetActor) { self.actor = actor }
    func send(_ work: @escaping Work) async {
        queue.append(work)
        if !isProcessing {
            isProcessing = true
            Task {
                while !queue.isEmpty {
                    let work = queue.removeFirst()
                    await work(actor)
                }
                isProcessing = false
            }
        }
    }
}

Used like

await actorQueue.send { myActor in await myActor.handle(completion: completion) }
1 Like

But then you need again a Task to use send in a Sync context.

Yeah, sorry, I guess I tried solving a different problem that nobody actually needs solving :grinning:

I didn't at all solve the problem of going from sync to async while keeping the order. One more reason to use AsyncSequence instead of Combine, I guess :slight_smile:

3 Likes

So I guess you need Old Style concurrency to solve that gap? E.g. in my example if you put queue and isProcessing in some sort of mutex then send() could be sync.

It looks like you potentially can workaround this actor limitation with something like this Messenger. It is similar to what @JJJ also posted:


class Messenger<A: Actor> {
    typealias Message = (A) async -> Void
    
    let messages = ThreadSafe(value: Array<Message>())
    let isProcessing = ThreadSafe(value: false)
    let actor: A
    
    init(actor: A) {
        self.actor = actor
    }
    
    func send(_ message: @escaping Message) {
        messages.withValue {
            $0.append(message)
        }
        isProcessing.withValue { processing in
            if !processing {
                startProcessing()
                processing = true
            }
        }
    }
    
    func startProcessing() {
        Task { [weak self] in
            while true {
                guard let self = self else { return }
                let work = self.messages.withValue { $0.popFirst() }
                if let work = work {
                    await work(self.actor)
                } else {
                    await Task.yield()
                }
            }
        }
    }
}


actor MyActor {
    var list: [Int] = []

    func add(_ i: Int) {
        list.append(i)
        print("- \(i)")
    }
}

and the usage would be something like this:

        ...
        let actor = MyActor()
        let messenger = Messenger(actor: actor)
        
        for i in 0...10 {
            messenger.send { actor in
                await actor.add(i)
            }
        }
        ...

Where ThreadSafe is basically an UnfairLock implementation.

This seems to work, but I can't be certain it's correct. And the obvious downside is that there is a spinning while loop with a Task.yield in it, to keep it alive. I'm not sure that's acceptable, but it might be the best we can do without something built-in.

Either way, to achieve something like this, the code is not trivial in my estimation.

It is not deterministic what order the actor gets those messages in, and we have no way to express “enqueue but don’t wait”, so the only way to get the ordering we want to so await on both calls in the current task.

This is really a bit problematic for some use cases.

To be able to use send instead of Task to just enqueue an item in the mailbox would help, I'd view the send to fundamentally be identical to an await that it allows calling an async function, but with the semantical difference that is not an suspension point, but just enqueue it in the mailbox (only valid for void functions). Basically it's a noawait. Is that a reasonable understanding?

The simplified case with two async lets are conceptually similar to if I would had two lines with pthread_create (sorry for mixing analogues, that's my mental model for Task creation) - I'd definitely need to await the completion of the first call if I want to guarantee sequencing on the actor call, and I can't see how even a 'send' actor operation would help in that case (unless you simply would remove the async let and just to send two lines after each other). I think one can't ever expect any dependencies or ordering between async lets without explicit await/sync in between.

1 Like

Has any progress been made on this in the last year? I, too, am running into a problem which is difficult to solve without a synchronous "enqueue this on the actor and then return" function.

1 Like

Nothing so far (meaning, in 5.7.0). I'll help us prove the importance of this if you could provide a snippet illustrating your use case -- thanks in advance! :slight_smile:

3 Likes

Okay, I'll try to reduce this to the essentials.

In ye olde days of dispatch queues, there was a pattern we could use to make a property atomic:

public var foo: Foo {
    get {
        queue.sync { /* get the value somehow */ }
    }
    set {
        queue.async { /* set the value somehow */ }
    }
}

The idea behind it was basically that as a consumer, the only time you really need to wait for the value is when you are reading it. When you are writing, all you really need is to know that the write is occurring, but you don't necessarily need to wait for it.

If we were able to enqueue things, it'd be possible to implement the equivalent of this pattern with actor-based storage; i.e. you would await when reading values from the actor, but you could skip the overhead of creating a suspension point and continuation when writing. This would also allow the use of an actor-based property from non-asynchronous code, if said code is purely producing data rather than reading it. It also would allow the write function to fulfill the requirements of a protocol, since many / most of the existing ones come from the pre-concurrency era and don't specify their functions to be async.

One last thing that's a lot simpler, but just really annoying: if I have a class or struct that's using an actor for storage, and I'm setting an initial value on the actor, it causes the initializer to need to be async:

init(initialValue: Foo) async {
    await self.actor.setValue(initialValue)
}

Any further access to the value is going to be protected by the actor anyway, so there's no real reason to wait for the setter, as long as it's been enqueued before the init function returns. The lack of this ability requires the initializer to be async (which, again, can mess with protocol requirements). One might think to wrap the setter in a Task, but then you run the risk of thread-unsafe access, if something initializes the object and then immediately awaits whatever accessor returns the actor's value. This would cause a race condition in which the consumer might read the value before the actor has initialized it.

Is there also an implicit expectation here that when there are no interleaving calls to the actor, that in the following sequence:

obj.foo = someFoo
print(await obj.foo)

the printed value is always the same as that of someFoo? Otherwise, it seems like the existing nonisolated modifier would allow you to implement something close to this pattern (though we don't currently support nonisolated set as separate from get). You'd have the nonisolated func setFoo(_: Foo) or whatever kick off a Task that asynchronously sets the (isolated) foo.

I think your init example sets up the necessity for enqueue-before-return nicely, just wanted to press a bit more for what's motivating this requirement in the general case.

1 Like

I’m not sure I understand this. await is more like DispatchQueue.async than DispatchQueue.sync. The overhead of a suspension point is the overhead of enqueueing, and the overhead of a continuation is the overhead of a block (conceptually, the implementation differs a bit of course). In order to implement that pattern you would need a “synchronous await” for the read side, which doesn’t exist.

(As an aside: the libdispatch engineers I’ve talked to about that pattern don’t recommend it in general. The overhead of the async, as you allude to, is usually significantly higher than the cost of setting the property)

2 Likes

Yes. The lookup on the await obj.foo call should not occur until after the setter has completed. And if the setter gets called several times in succession, the async should be enqueued in the order the setters were called.

In this case, the analogue to dispatch_async is that the calling code does not need to wait for the setter to finish before it continues on its way.

You are correct that the reader here would need to be async. The idea here is to allow a data provider, which is dumb and does not necessarily know about Swift async (perhaps it's a callback from a C function, a conformance to some protocol that doesn't allow its members to be async, or some other similar thing) to feed data to this thing without having to wait for the setter, or even know/care that the setter is doing something async.