Calling async functions of actor members

Hi there,

I am endeavoring with actor-facade for spotlight indexing.

In apple doc, in description of a CSSearchableIndex class, I read that

>>> Modify custom CSSearchableIndex objects only on one thread or task at a time. It’s a programming error to access a custom index from multiple threads simultaneously.

Oh! It seems that is an ideal situation to use actor as a facade to control an access to CSSearchableIndex object.
But, when I wrote a simple actor facade for indexing, I saw this:

actor SpotlightIndex {
    private let searchableIndex: CSSearchableIndex

    init(searchableIndex: CSSearchableIndex) {
        self.searchableIndex = searchableIndex
    }

    func indexItems(_ items: sending [CSSearchableItem]) async throws {
        try await searchableIndex.indexSearchableItems(items) /// ❌ Sending 'self.searchableIndex' risks causing data races; this is an error in the Swift 6 language mode
        /// Sending 'self'-isolated 'self.searchableIndex' to nonisolated instance method 'indexSearchableItems(_:)' risks causing data races between nonisolated and 'self'-isolated uses
    }

    func deleteItems(with identifiers: sending [String]) async throws {
        try await searchableIndex.deleteSearchableItems(withIdentifiers: identifiers) /// ❌ Sending 'self.searchableIndex' risks causing data races; this is an error in the Swift 6 language mode
        /// Sending 'self'-isolated 'self.searchableIndex' to nonisolated instance method 'deleteSearchableItems(withIdentifiers:)' risks causing data races between nonisolated and 'self'-isolated uses
    }
}

These compiler errors seem actual - nonisolated functions take actor member as a parameter (because of implicit self access), and nobody knows how does this member changes inside nonisolated func...

Is it possible to resolve this error someway?

Since you do not control this API and without default inheritance for nonisolated async functions, your only option might be to use the historical interfaces that use completion handlers instead of async/await.

1 Like

Your bigger issue here is that it doesn't do what you want. An actor only protects against data races. When an actor awaits anything, it is allowed to run other functions inside the same actor before the await returns.

The same would be doubly true if you used completion handler, because while you wait for the completion handler to be called, other actor methods can be called, despite it being disallowed.

What you need is some sort of async mutex, or a queue that accepts async functions and awaits them before processing the next item. You then wrap searchableIndex in one of these, to make it Sendable. An actor isn't going to do that for you.

1 Like

To be honest, in my opinion actors are some sort of syntax sugar around serial queues. In structured concurrency, there is no term like queue and locks in GCD, but developers need some tools to provide exclusive access. Doesn't matter for what - for protect mutable state (against data races) or to provide exclusive thread access (like my case). Am I wrong?

I've tried to find something like async mutex, but I didn't have success. It looks like the documentation mean that the execution of code between function invoke and a moment when function goes in internal async waiting must be exclusive. What happens after - doesn't matter, because completion block doesnt' have any access to CSSearchableIndex ; it's only for result update - index operation is already over by that time. If my assumption is right, than actor is suitable for my case. Am I wrong again? :slight_smile:

You just need to be aware it is possible to reenter these methods before the completion handlers are called. You might want to make sure this does not happen or maybe it's not an issue, that will depend on the API contract of CSSearchableIndex and your app logic.

Close, but not quite, because DispatchWorkItem can only hold a sync operation. If you want an async operation, you need to not only cause it to start on the same thread as the dispatch, but also make it switch back to that thread after every await. This is doable if your async operation is written as a bunch of completions, but not with a pure async method.

Even if it was possible, the compiler wouldn't be able to reason about data races in such a manually written handling of thread switching. So it's more than just sugar.

I'm not seeing anything in the documentation to suggest that. However, if that is the case, you can simply use the completion handler version, having the completion handler spawn a task that calls an actor's method, performing the isolation "switch back". However, you lose the ability to await until the task is finished, unless also adding a manual continuation.

Your real issue is that despite being inside an actor, you can actually call deleteItems while indexItems is being awaited in another task. Because actors are sendable, this code becomes valid:

let index = SpotlightIndex(CSSearchableIndex(name: "MyIndex"))
async let parallel: () = {
  await index.indexItems([/* ... Abridged for brevity ... */])
}
await index.deleteItems(with: ["Identifier"])

As a result, deleteSearchableItems can be called while indexSearchableItems is still being awaited. And Swift has no way to know if that should be allowed.

There's no built-in solutions, but there are a bunch of libraries that achieve it. The second answer to this StackOverflow question contains a naive implementation of an async lock using actor. While the first answer explains how to protect a value using a lock. Combining the two you can create a locked box that would allow you to hold a non-Sendable value while being able to safely access it.

With a little effort using sending and inherited isolation, you might even be able to achieve it without needing @unchecked Sendable.

I've tried to find something like async mutex, but I didn't have success.

Here is a semaphore implementation for swift concurrency

And here is a serial queue implementation for swift concurrency:

Maybe that will help.

1 Like

If nonisoalted async func inherits caller's isolation domain, I think it's OK to use actor in this case, because all of CSSearchableIndex's async methods run in the actor's isolation domain and none are interleaved.


While thinking about this, I realize a question. For APIs that are supposed to be used in an actor (e.g., CSSearchableIndex in this case), isn't it better for Apple to provide these APIs as synchronous functions? That would make it simple to call them in actor.

As mentioned above, it does not. Not unless the relevant pitch linked above is implemented.

And there's a question as to whether that pitch would be backportable to older libraries, since it depends on an implicit isolation parameter that a pre-concurrency async function might not provide.

Not really, because you'd be potentially blocking the thread on IO. Remember, just because you're not allowed to use a class/resource, does not mean it's holding a thread. IO can easily wait on several requests to different handles on a single thread [1], while it still being invalid to send multiple IO requests to the same handle.

P.S. Adding to the above, if the async versions are simply wrappers for the completion handlers, inheriting isolation is not going to help anyway. Because only the call and the completion handler would be running on the actor thread. Something like IO, for instance, would be running on the OS kernel, probably being waited on in a dedicated thread. So, it would not block the actor's thread either way.
And indeed it shouldn't. Rather, the actor should manage its own queue to serialize calls to the underlying resource, and not waste a thread busy-waiting for the operation to complete.


  1. Thanks to epoll, kqueue, etc ↩︎

2 Likes