I have a singleton class where I make a network call to initialise it. I want to make sure that load()
is not called concurrently - I can cache the results and avoid unnecessary network round-trips. In the old world of GCD, I could use a DispatchQueue, DispatchGroup or a DispatchSemaphore to achieve this. In the new world of async/await, Actors seem to be the way to go. But I can't get it to work.
Example
With actors, only one function can run at a time - unless an async call is part of the function.
Let's define a log function for convenience:
func log(_ string: String) {
print("\(Date.now) - \(string)")
}
And then compare the following two actors:
actor AsyncActor {
static var shared = AsyncActor()
func load() async throws {
log("AsyncActor load() start")
try await Task.sleep(nanoseconds: 1_000_000_000)
log("AsyncActor load() end")
}
}
Task { try? await AsyncActor.shared.load() }
Task { try? await AsyncActor.shared.load() }
actor SyncActor {
static var shared = SyncActor()
func load() async throws {
log("SyncActor load() start")
sleep(1)
log("SyncActor load() end")
}
}
Task { try? await SyncActor.shared.load() }
Task { try? await SyncActor.shared.load() }
Output:
2022-10-09 13:45:55 +0000 - AsyncActor load() start
2022-10-09 13:45:55 +0000 - SyncActor load() start
2022-10-09 13:45:55 +0000 - AsyncActor load() start
2022-10-09 13:45:56 +0000 - SyncActor load() end
2022-10-09 13:45:56 +0000 - SyncActor load() start
2022-10-09 13:45:56 +0000 - AsyncActor load() end
2022-10-09 13:45:56 +0000 - AsyncActor load() end
2022-10-09 13:45:57 +0000 - SyncActor load() end
(You can try this in a playground)
The same async code with semaphores works:
struct AsyncSemaphore {
let semaphore = DispatchSemaphore(value: 1)
static var shared = Self()
func load() async throws {
log("AsyncSemaphore load() barriers")
semaphore.wait()
defer { semaphore.signal() }
log("AsyncSemaphore load() start")
try await Task.sleep(nanoseconds: 1_000_000_000)
log("AsyncSemaphore load() end")
}
}
Task { try await AsyncSemaphore.shared.load() }
Task { try await AsyncSemaphore.shared.load() }
Output:
2022-10-09 13:49:24 +0000 - AsyncSemaphore load() barriers
2022-10-09 13:49:24 +0000 - AsyncSemaphore load() barriers
2022-10-09 13:49:24 +0000 - AsyncSemaphore load() start
2022-10-09 13:49:25 +0000 - AsyncSemaphore load() end
2022-10-09 13:49:25 +0000 - AsyncSemaphore load() start
2022-10-09 13:49:26 +0000 - AsyncSemaphore load() end
I'd normally be happy with this, but semaphore.wait()
triggers the following warning in Xcode:
Instance method 'wait' is unavailable from asynchronous contexts; Await a Task handle instead; this is an error in Swift 6
Question
It seems that Actors do not provide the form of protection from concurrent execution that I want. How can I achieve with Async/Await the same behaviour I achieved with DispatchSemaphore
?