With settings above, following code gives me an error:
final class Tester {
var cancellables: Set<AnyCancellable> = []
init() {
NotificationCenter.default
// Error:
// Main actor-isolated class property 'didEnterBackgroundNotification' can not be referenced from a non-isolated context
.publisher(for: UIApplication.didEnterBackgroundNotification)
.sink { [weak self] _ in
self?.test()
}
.store(in: &self.cancellables)
}
private func test() {
print("Tested")
}
}
If I wrap the subscription code in an @MainActor closure, it prompts a warning:
init() {
+ Task { @MainActor in
NotificationCenter.default
.publisher(for: UIApplication.didEnterBackgroundNotification)
+ // Warning:
+ // Capture of 'self' with non-sendable type 'Tester' in a `@Sendable` closure
.sink { [weak self] _ in
self?.test()
}
.store(in: &self.cancellables)
+ }
}
If I conforms the Tester class to Sendable, another warning prompts:
-final class Tester {
+final class Tester: Sendable {
+ // Warning:
+ // Stored property 'cancellables' of 'Sendable'-conforming class 'Tester' is mutable
var cancellables: Set<AnyCancellable> = []
...
}
I understand that the errors and warnings go away if I mark the Tester class with @MainActor or conforms it to @unchecked Sendable. But I don't want do either, because none of its functions or initializers needs to be called on main thread.
You can’t make mutable types (or types that contain mutable types) Sendable.
Generally it is better not to use Combine and structured concurrency together. They are really orthogonal features.
I think you need to either put the actor outside the function to make it the callers responsibility to run on the MainActor or use a closure that doesn’t use structured concurrency:
init() {
// Your responsibility to run on MainActor
Task {
await MainActor.run { … Combine stuff here … }
// ^ This closure isn’t async so structured concurrency isn’t mixed with combine
}
}
// Or
// callers responsibility to run on MainActor
@MainActor init() {
}
The demo code posted here does not use structured concurrency, the error/warning is prompted due to UIApplication being marked as @MainActor which leads to the UIApplication.didEnterBackgroundNotification being required to be used in a "Main actor isolated" context. Because Tester being a class, does not have any shared data for accessing, and its work does not need to, or must not be performed on main thread to block the UI.
I don't think the code you posted in the reply will resolve this issue.
It does though: you have var cancellables. Any mutable property on a class is considered shared mutable state. If you don't need it to be mutable outside of the init, you could say
final class Tester: Sendable {
// using 'let' here means it's not considered shared mutable state
let cancellables: Set<AnyCancellable>
init() {
var cancellables = Set<AnyCancellable>()
// ... init body ...
self.cancellables = cancellables
}
}
Really though, I don't see why UIApplication.didEnterBackgroundNotification needs to be main-actor-isolated. It's just a constant Notification.Name(rawValue: "UIApplicationDidEnterBackgroundNotification").
That is only for holding the subscription. The problem still persists if we get rid of it.
final class Tester {
init() {
NotificationCenter.default.addObserver(
/// Error:
/// Main actor-isolated class property 'didEnterBackgroundNotification' can not be referenced from a non-isolated context
forName: UIApplication.didEnterBackgroundNotification,
object: self,
queue: .current
) { _ in
print("Notification received")
}
}
}
Really though, I don't see why UIApplication.didEnterBackgroundNotification needs to be main-actor-isolated.
My point exactly. I think it should be excluded from @MainActor's scope.
@MainActor is using structured concurrency. That is fine (and preferred) as long as you are careful to keep your synchronous code out of asynchronous contexts where they need to be sendable. In this case, you would just dispatch a task to the main thread using a main actor (since objective-c code often needs that for historical reasons) then use your combine code completely from synchronous contexts where it doesn’t require Sendable. MainActor.run { } basically switches you from async to synchronous.
I don’t believe NotificationCenter has significant thread safety issues–particularly when used with a Combine publisher. That said, it is often a good idea to run old objective-c APIs on the main actor to be safe.
Thanks again, but honestly, I think you are missing the point here. The question is how to avoid occupying the main thread when unnecessary. When we observe/subscribe to a notification, it is not necessary to perform the observation/subscription on the main thread, even if that notification is marked with @MainActor. For example,
final class Tester: Sendable {
init() {
Task { @MainActor in
_ = NotificationCenter.default.addObserver(
forName: UIApplication.didEnterBackgroundNotification,
object: nil,
queue: .current
) { _ in
print("Notification received")
}
}
}
}
Surely, the code above works. But it is not right.
First, neither NotificationCenter.default.addObserver(:::) nor NotificationCenter.default.publisher(for:) should be forced on main thread, and probably better not to.
Second, the subscription/observation should be finished before the initialization finishes, but the current implementation forces asynchrony, meaning it is not guaranteed to be ready when using a Tester instance, because the subscription/observation is done asynchronously.
I disagree, and I don't see how the problem is related to old objective-c APIs.
I'm not sure why you would want to subscribe in an asynchronous context. Normally NotificationCenter would have the subscription created in a synchronous context. It is very fast, so that is fine. Generally unsubscribing in a dealloc. If you need the work do be done to be async, you would start a task in the callback.
You just want to avoid larger amounts of work in the main thread. Trivial work of registering observers is fine since it will immediately return to the run loop. Under the hood it will need to serialize access to its data structure, which is likely simply running on the main thread. That is very common when you are applying something that can't be touched by other threads.
Similarly, with Combine you would register that in a synchronous context and then use .receive(on: ...) to run it in the background.
I also wouldn't worry too much about taking trivial things off the main thread. The most important thing is to avoid stalling the main thread. Even if you were to do a large amount of work, you could just divide the work up in to chunks that are separately dispatched to the main thread and it should be fine.
Please, I am not trying to subscribe/observe in asynchronous context. The asynchrony is trying to eliminate the error/warning in the original demo code, which has nothing to do with structured concurrency except for the notification name itself.
Ok, I see where you are coming from. Apologies for misunderstanding. A lot of Apple's Objective-C SDKs, like what you are getting in UIKit, are not ready for strict concurrency checking. So you need to put that in an isolated context if you want it to pass. You could set that to Targeted if you just want stricter testing on newer code. You could also just guarantee it is isolated like the following:
final class Tester {
var cancellables: Set<AnyCancellable> = []
@MainActor init() {
NotificationCenter.default
// No longer any error here since didEnterBackgroundNotification is now on the main actor.
.publisher(for: UIApplication.didEnterBackgroundNotification)
.sink { [weak self] _ in
self?.test()
}
.store(in: &self.cancellables)
}
private func test() {
print("Tested")
}
}