Can actor instance functions interleave each other?

  1. If no actor instance-level function have await in them, then the calls will be executed in the same order they were originally posted, right?
  2. If an actor instance-level function does have at least one await in it, the code that takes over can be a different function call of the same receiver? (I’m guessing/fearing the answer is “yes.”)
  3. If the answer to (2) is “yes,” then how should a function have to wait for earlier methods from the same receiver to complete? (This is for an actor for an Internet connection, where each method is a separate protocol transaction, so the transactions are serialized.)
1 Like

The calls within that function will run sequentially, but if you have some other code that calls this function, you do not have assurances that they will be performed in the order that they were awaited. I draw your attention to an “Implementation note” in SE-0306 – Actors.

You asked:

Your fear is warranted, as actors are reentrant. See the Actor reentrancy discussion from SE-0306.

One elegant approach is to use an AsyncChannel from the Swift Async Algorithms package. E.g., let’s say you want to download assets one at a time, in a strict FIFO the order that they were originally requested, you might have an AsyncChannel<URL>, and have a routine that monitors this channel, downloading assets in the order that they were “sent” to that channel. That is FIFO and sequential. See this StackOverflow answer for an example.

The more general (and more complicated) approach would be to save a reference to the prior Task. E.g.:

actor Foo {
    private var previousTask: Task<String, Error>?

    func requestValue(for index: Int) async throws -> String {
        let task = Task { [previousTask] in
            try await withTaskCancellationHandler {
                let _ = try await previousTask?.value
            } onCancel: {
                previousTask?.cancel()
            }

            return try await perform(for: index)
        }

        previousTask = task

        return try await withTaskCancellationHandler {
            try await task.value
        } onCancel: {
            task.cancel()
        }
    }
    
    func perform(for index: Int) async throws -> String {
        try await Task.sleep(for: .seconds(1))
        
        …

        return …
    }
}

Here my tasks are returning some String, but you can use this pattern with any Sendable type.


I should note that the performing of requests sequentially like this can have serious performance implications, where the natural network latency of individual requests can start to add up. So you might want to really think about whether you want/need all requests serialized, or whether only some requests need to await others whereas others should be free to run concurrently. E.g., if doing an “authentication” request followed by 10 downloads, you might only await the Task associated with the initial authentication request, but then allow the 10 downloads to run concurrently. It depends upon the details of your requirements, but I might advise appreciating the performance implications of forcing all requests to await prior ones.

7 Likes

I wish TSPL Book had addressed this topic in detail.

1 Like

For high-level code, concurrent downloads should be better. But I'm writing the low-level code that actually does the work, so I do have forced sequential-ness. (That high-level code with 10 concurrent downloads would call 10 separate instances of my code.)