Hello,
I have deeply enjoyed using actors and async/await in my codebase, and decided to take a look at Swift 5.6 stricter concurrency checks would do to it.
The good news is, the Sendable checks indirectly found a couple of mistakes in my code, but in other cases, I am not sure whether my solutions are correct, the compiler checking is correct, or if I generally misunderstand the philosophy of using actors.
I am using an actor to funnel all API calls to a native library that has certain threading requirements, and I am very happy with the results.
Here are the notes that I took with my struggles, hopefully these will be useful to the language designers, and maybe someone can steer me in the right direction:
Sendable Challenges
I am calling into an actor method with await, like this:
class Identity {
var username: String
}
actor myActor {
func method (name: String) {}
}
class Demo {
var identity: Identity
func stuff () async {
await myActor.method (name: identity.username)
}
}
The above call to method produces this error:
cannot use property 'identity' with a non-sendable type 'Identity' across actors
Ok, the issue is that I am accessing the identity from the async function, and it is not itself sendable, so I am guessing it could be poked by externally?
Weak Fields
The following is tricky:
class Demo: Sendable {
weak var other: MyActor!
}
It complains that other is mutable. The challenge is that I do not want to introduce a strong cycle by keeping a reference to the actor. Does this mean that actors should not participate in these cycle prevention things?
Observation: this I sort of worked around in one case, I made it strong, but it makes me wonder about how I would use this idiom in practice.
Multi-stage initialization
I need to initialize my Actor twice, not because I want, but because I have not found any other way to solve this.
I am interoperating with a C library that takes a closure and passes this closure to callbacks for these callbacks to find their purpose in life. What I do is that I create an opaque objects that wraps self. But because I am referencing self, I have to first initialize my actor variable, so I initialize it with a fake actor first, then grab self, then reset it.
But this produces the following error:
Stored property 'actor' of 'Sendable'-conforming class 'Demo' is mutable
actor MyActor {
init (fakeSetup: Bool) {}
init (handle: OpaquePointer) {
call_c_library_with_handle (handle)
}
}
class Demo: Sendable {
var actor: MyActor
init () {
actor = MyActor (fakeSetup: true)
let handle = opaqueHansle = UnsafeMutableRawPointer (mutating: Unmanaged.passUnretained (self).toOpaque ())
actor = MyActor (handle: handle)
}
}
Passing Data to an actor
It does not seem possible to pass Data to an actor, even if it is a let Data. It seems like either Data should be sendable, or the fact that the Data is a constant and not used later taken into consideration:
func cuteApi (data: Data) {
// We know here that we can not poke at 'data' at all, so why is this a problem?
myActor.doSomething (with: data)
}
DispatchQueue.main.async
In various places, I use async methods like this:
class MyView: UIView {
var label: UIView
func someCuteBackgroundMethod () async {
workWork ()
// Update the UI
DispatchQueue.main.async {
self.label.text = "Done"
}
}
}
But I get the following error:
Capture of 'self' with non-Sendable type 'MyView' in a @Sendable closure
No problem, I went and slapped a Sendable on my UIView, but I am wondering if this is the right thing to do. It did get rid of a ton of issues, so I am crossing fingers that this was the right move.
Callbacks are problematic
In a number of places I pass a callback that I need to invoke at a later point, like these:
class Demo: Sendable {
let readCallback: (Demo demo)async ->()
init (readCallback: (Demo) async->()) {
self.readCallback = readCallback
}
func doStuff () async {
await readCallback (self)
}
}
But the compiler complains like this:
Stored property 'readCallback' of 'Sendable'-conforming class 'Demo' has non-sendable type '(Demo) async â ()'
I do not understand why this is a problem, the Demo class itself is sendable, as in, it is safe to access its properties concurrently, given than the readCallback is a let variable, initialized only once. I am puzzled by this error.
Quick hacks
To get over a bunch of errors, I added Sendable conformances to various built-in types, these are dubious decisions, but they were done based on my assumption that I knew how I was using them, and I believe their usage is correct (due to the fact that I do not mutate them), or their purpose is to help me implement Sendability, these are:
- Data
- SecKey
- DispatchQueue
- DispatchSemaphore