The actor and async/await introduced into Swift recently seem to be a good thing, but even after trying and searching quite intensively for solution I am still not able to produce a simple example command line program combining actors with concurrent code. And a good documentation of how to implement concurrent code seems to be missing.
I will give a little longer example below, which I think this is necessary to explain the problem.
At the end I will point out to another issue, that "async" methods so-to-say can "pollute" a whole code base with sync/await.
Of course, why a specific code is not correct or why the aforementioned "pollution" is logical is somehow clear, but it is not clear (at least to me) what a sensible solution to the underlying problem could be.
Actors in Concurrent Code
To my understanding, an actor controls asynchronous acces to a resource (e.g. a variable):
Example:
We could have a method "say" without such a control, together with a function "work" that calls this method:
class Sayer {
// non-exclusive access to a resource:
func say(_ text: String) {
print(text)
}
}
func work(_ id: String) {
for i in 1...3 {
sayer.say("\(id) #\(i)")
sleep(UInt32(Int.random(in: 1...2))) // simulating some real work
}
}
We could also use an actor, so "say" can only be executed once in a time, and a caller becomes suspended until its call to to "say" actually executes:
actor AsyncSayer {
// exclusive access to a resource needed:
func say(_ text: String) {
print(text)
}
}
func workAsync(_ id: String) async {
for i in 1...3 {
await asyncSayer.say("\(id) #\(i)")
sleep(UInt32(Int.random(in: 1...2))) // simulating some real work
}
}
We could very well call "workAsync" without actually doing anything in parallel:
@main
struct AsyncTest1 {
static func main() async {
let workItems = ["A", "B"]
await workItems.forEachAsync { workItem in
// when we use workAsync instead, we get the following compiler error:
// "'async' call in a function that does not support concurrency"
await workAsync(workItem)
}
}
}
using
extension Sequence {
func forEachAsync (
_ operation: (Element) async throws -> Void
) async rethrows {
for element in self {
try await operation(element)
}
}
}
Of course, in such a context the definition of our actor is not really a sensible thing, since "say" is called only once at a time anyway.
Let's do some work concurrently:
@main
struct AsyncTest2 {
static func main() async {
let workItems = ["A", "B"]
let group = DispatchGroup()
workItems.forEach { workItem in
group.enter()
queue.async {
// when we use "workAsync" instead, we get the following compiler error:
// "'async' call in a function that does not support concurrency"
work(workItem)
group.leave()
}
}
group.wait()
}
}
Here, the work is done in a concurrent way, but using the version of "say" which does not (!) control access.
If we would use the version of "say" which does control access, we would have to call "workAsync", but then – as noted in the last code – the compiler says "'async' call in a function that does not support concurrency".
The asnyc/await-"pollution"
I hope that the word "pollution" does not sound too negative, as asnyc/await is a great solution to many problems. What do I mean by "pollution"? Well, when you have code that has to accessed by "await", this code itself can be suspended and has to be marked "async" and called using "await".
OK, this is really seems to be a logical consequence. But compare this to code manipulating state in the Haskell programming language. Haskell "in its pure form" is stateless. Using state anywhere inside a Haskell program that uses state "pollutes" the whole program with this state. But there exists the "State Monad" which can hide state from the rest of a program. I wish there would be such a thing for "async/await" in Swift. ... OK, this quite imprecise, but maybe you see what I mean and someone with a better understanding of these things can say a word about it.
Conclusion
As said at the beginning, some good documentation about the current (or future) state of concurrency in Swift, with working basic examples (!), seems to be missing. Or can anyone point out to a good documentation of this kind (serious question, I would really like to know)? If there is really documentation missing here, maybe some smart people could write one...
At least I would be very grateful if some someone could give me a working example. Thanks in advance!