Help Understanding Concurrency Error with Protocol Listener and Actor

Hi all,

I'm running into a Swift Concurrency issue and would appreciate some help understanding what's going on.

I have a protocol and an actor set up like this:

protocol PersistenceListener: AnyObject {
func persistenceDidUpdate(key: String, newValue: Any?)
}
actor Persistence {
func addListener(_ listener: PersistenceListener) {
listeners.add(listener)
}
/// Removes a listener.
func removeListener(_ listener: PersistenceListener) {
listeners.remove(listener)
}
// MARK: - Private Properties
private var listeners = NSHashTable<AnyObject>.weakObjects()
// MARK: - Private Methods
/// Notifies all registered listeners on the main actor.
private func notifyListeners(key: String, value: Any?) async {
let currentListeners = listeners.allObjects.compactMap { $0 as? PersistenceListener }
for listener in currentListeners {
await MainActor.run {
listener.persistenceDidUpdate(key: key, newValue: value)
}
}
}
}

When I compile this code, I get a concurrency error:

"Sending 'listener' risks causing data races"
Answered by DTS Engineer in 832932022

Right. That’s because:

  1. You got listener from one context, that of the notifyListeners(…) async function.

  2. You’re passing it to another context, the main actor.

  3. And listener is not sendable.

How you fix this depends on how you want the code to behave. The two most common choices are:

  • If you don’t want to constrain the context in which listeners are called, force PersistenceListener to be sendable.

  • If you always want the listeners to be called on the main actor, decorate everything with @MainActor.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

Accepted Answer

Right. That’s because:

  1. You got listener from one context, that of the notifyListeners(…) async function.

  2. You’re passing it to another context, the main actor.

  3. And listener is not sendable.

How you fix this depends on how you want the code to behave. The two most common choices are:

  • If you don’t want to constrain the context in which listeners are called, force PersistenceListener to be sendable.

  • If you always want the listeners to be called on the main actor, decorate everything with @MainActor.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

Hello, Thanks for your reply!

Yeah, I’ve already figured that part out, but there’s one more issue: the value also needs to be sendable. I picked the simplest example, but this new concurrency model adds even more boilerplate code. Honestly, I’m finding this more challenging than working with ObjC back in 2009 🙂

Written by olonsky in 833063022
there’s one more issue: the value also needs to be sendable.

Which value are you talking about here? The value parameter of notifyListeners(…)?

If so, that has to be sendable if you adopt the first choice I listed. If not, then you can avoid that constraint because you never cross between isolation domains (everything is bound to the main actor).

Written by olonsky in 833063022
I’m finding this more challenging

It’s definitely a challenge, but the more I work on this stuff the more I realise how important it is for the compiler to check this stuff for you. For example, I was recently stymied by the fact that Foundation’s Thread isn’t sendable. I’ve written Objective-C and pre-Swift 6 code for decades that just assumed that it was. How many weird crashes has that code caused over the years?

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

I'm not a fan of annotating everything with @MainActor. So yes, in the case of Sendable, the newValue must also conform to Sendable.

I’ve written Objective-C and pre-Swift 6 code for decades that just assumed that it was. How many weird crashes has that code caused over the years?

Yeah, I totally get the value of having the compiler catch these things, it's a solid direction. Personally, I can't say I've run into that many crashes from threading issues in practice, at least judging by crash metrics, things have been pretty stable. I just find it hard to get excited about spending too much time fixing compiler errors :)

Help Understanding Concurrency Error with Protocol Listener and Actor
 
 
Q