I have an actor:
actor MyActor {
let theQueue = OperationQueue()
init() {
_ = theQueue.observe(\OperationQueue.operationCount, options: .new) { oq, change in
print("OperationQueue.operationCount changed: \(self.theQueue.operationCount)")
}
}
....
}
I was trying to get a KVO going to then trigger some type of publisher call that other models in the app could subscribe to and react as needed when the operationCount changes.
I was going to have a function that maybe would set that up, but, as of now, using self in that initializer gives me this warning, which according this this:
https://forums.swift.org/t/proposal-actor-initializers-and-deinitializers/52322
it will turn into an error soon.
The warning I get is this:
Actor 'self' can only be captured by a closure from an async initializer
So, how could I trigger a publisher other models can then react to that would publish the operation queue's operationCount as it changes?
You don't need to capture self here. observe sends you the new value (for basically exactly this reason):
_ = theQueue.observe(\OperationQueue.operationCount, options: .new) { oq, change in
print("OperationQueue.operationCount changed: \(change.newValue!)")
}
Also, oq is theQueue if you need that. If you need self, the typical way to do that is:
observation = observe(\.theQueue.operationCount, options: .new) { object, change in
// object is `self` here.
}
Just remember that you're outside the actor inside this closure, so calls may need to be async inside a Task.
Related
I have a GlobalActor with some methods on it that I'm using throughout my app. I'd like to be able to call a function from the actor when I receive a Notification from NotificationCenter, but can't call async functions from sink.
This is what I'm doing now:
class MyClass {
private var cancellables: [AnyCancellable] = []
init() {
NotificationCenter.default.publisher(for: NotificationName)
.receive(on: DispatchQueue.global(qos: .utility))
.compactMap { $0 as? SomeType }
.sink { [weak self] val in
Task { [weak self] in
await self?.someCallToActor(val)
}
}.store(in: &cancellables)
}
#SomeGlobalActor
func someCallToActor(_ val: String) async {
await SomeGlobalActor.shared.actorMethod(val)
}
}
...
#globalActor
actor SomeGlobalActor {
static var shared = SomeGlobalActor()
func actorMethod(_ val: String) async {
...
}
}
Calling Task within a closure here feels wrong and potentially race-condition-y. Is this the best way to accomplish what I'm trying to? I've tried receiving the notifications inside of the actor itself but it doesn't change much. The issue is the closure provided to sink is meant to be synchronous so I can't await inside of it.
The only way to get the Actor to do something is to put a message in its mail queue. The actor handles messages one at a time, in the order received. Every message that goes into the queue gets a response. Code can only put a message in the queue if it's willing, and able, to wait around for the response. The sink function can't wait around, it has other things to do (i.e. handle the next incoming messages from a Publisher). It needs an intermediary to do the waiting for it. The Task is that intermediary.
Note that the actor only prevents race conditions on the actor's state. As your intuition suggests, you could still have the "high-level" race condition of two messengers (two Tasks) racing to see who puts their item in Actor's mail queue first. But within the actor, there will be a strict ordering to the changes made by the two messages. (preventing low-level data races on the Actor's state)
Unfortunately the order of execution of independent Tasks, like the individual tasks created by your sink, is arbitrary. Your code could process notifications out-of-order.
To solve the problems you need to serialize the order in which the notifications are received and then delivered to the actor. To do that you need one Task, one messenger, doing both jobs – receiving the notifications and passing them on to the actor.
NotificationCenter allows you to receive the notifications as an AsyncSequence. So instead of getting messages as a publisher, you could get them from a sequence. Something like this:
class MyClass {
let notificationTask: Task<Void, Never>
init() {
notificationTask = Task {
for await notification in NotificationCenter.default.notifications(named: interestingNotification) {
guard !Task.isCancelled else { return }
if let value = notification.userInfo?[0] as? String {
await someActor.actorMethod(value)
}
}
}
}
}
Here the Task waits to receive a message from the notification center. When it gets one, it does some transformations on it (pulling values out of userInfo in this case) then it hands the transformed message over to the actor. The notifications arrival is serialized by the async sequence and the task makes sure that they arrive to the actor in the same order.
In Swift, let’s say we have an actor, which has a private struct.
We want this actor to be reactive, and to give access to a publisher that publishes a specific field of the private struct.
The following code seems to work. But it produces a warning I do not understand.
public actor MyActor {
private struct MyStruct {
var publicField: String
}
#Published
private var myStruct: MyStruct?
/* We force a non isolated so the property is still accessible from other contexts w/o await in other modules. */
public nonisolated let publicFieldPublisher: AnyPublisher<String?, Never>
init() {
self.publicFieldPublisher = _myStruct.projectedValue.map{ $0?.publicField }.eraseToAnyPublisher()
}
}
The warning:
Actor 'self' can only be passed 'inout' from an async initializer
What does that mean? Is it possible to get rid of this warning? Is it “dangerous” (can this cause issues later)?
Note:
If I use $myStruct instead of _myStruct.projectedValue, it does not compile at all. I think it’s related, but I don’t truly see how.
The error is in that case:
'self' used in property access '$myStruct' before all stored properties are initialized
The warning is because when you are using _myStruct.projectedValue compiler notes that you are trying to do mutation on the actor from a synchronous context. All you have to do to remove the warning is to make your initializer asynchronous using async specifier.
The actor initializer proposal goes into more detail for why initializer needs to be asynchronous if there mutations inside actor initializer. Cosider the following case:
actor Clicker {
var count: Int
func click() { self.count += 1 }
init(bad: Void) {
self.count = 0
// no actor hop happens, because non-async init.
Task { await self.click() }
self.click() // 💥 this mutation races with the task!
print(self.count) // 💥 Can print 1 or 2!
}
}
Since there are two click method call one from Task and one called directly, there might be a case that the Task call executed first and hence the next call need to wait until that finishes.
I've created a Combine publisher chain that looks something like this:
let pub = getSomeAsyncData()
.mapError { ... }
.map { ... }
...
.flatMap { data in
let wsi = WebSocketInteraction(data, ...)
return wsi.subject
}
.share().eraseToAnyPublisher()
It's a flow of different possible network requests and data transformations. The calling code wants to subscribe to pub to find out when the whole asynchronous process has succeeded or failed.
I'm confused about the design of the flatMap step with the WebSocketInteraction. That's a helper class that I wrote. I don't think its internal details are important, but its purpose is to provide its subject property (a PassthroughSubject) as the next Publisher in the chain. Internally the WebSocketInteraction uses URLSessionWebSocketTask, talks to a server, and publishes to the subject. I like flatMap, but how do you keep this piece alive for the lifetime of the Publisher chain?
If I store it in the outer object (no problem), then I need to clean it up. I could do that when the subject completes, but if the caller cancels the entire publisher chain then I won't receive a completion event. Do I need to use Publisher.handleEvents and listen for cancellation as well? This seems a bit ugly. But maybe there is no other way...
.flatMap { data in
let wsi = WebSocketInteraction(data, ...)
self.currentWsi = wsi // store in containing object to keep it alive.
wsi.subject.sink(receiveCompletion: { self.currentWsi = nil })
wsi.subject.handleEvents(receiveCancel: {
wsi.closeWebSocket()
self.currentWsi = nil
})
Anyone have any good "design patterns" here?
One design I've considered is making my own Publisher. For example, instead of having WebSocketInteraction vend a PassthroughSubject, it could conform to Publisher. I may end up going this way, but making a custom Combine Publisher is more work, and the documentation steers people toward using a subject instead. To make a custom Publisher you have to implement some of things that the PassthroughSubject does for you, like respond to demand and cancellation, and keep state to ensure you complete at most once and don't send events after that.
[Edit: to clarify that WebSocketInteraction is my own class.]
It's not exactly clear what problems you are facing with keeping an inner object alive. The object should be alive so long as something has a strong reference to it.
It's either an external object that will start some async process, or an internal closure that keeps a strong reference to self via self.subject.send(...).
class WebSocketInteraction {
private let subject = PassthroughSubject<String, Error>()
private var isCancelled: Bool = false
init() {
// start some async work
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
if !isCancelled { self.subject.send("Done") } // <-- ref
}
}
// return a publisher that can cancel the operation when
var pub: AnyPublisher<String, Error> {
subject
.handleEvents(receiveCancel: {
print("cancel handler")
self.isCancelled = true // <-- ref
})
.eraseToAnyPublisher()
}
}
You should be able to use it as you wanted with flatMap, since the pub property returned publisher, and the inner closure hold a reference to self
let pub = getSomeAsyncData()
...
.flatMap { data in
let wsi = WebSocketInteraction(data, ...)
return wsi.pub
}
I have an observable inside a function.
The function happens in a certain queue, queueA, and the observable is subscribed to with observeOn(schedulerB). In onNext, I'm changing a class variable.
In another function, I'm changing the same class variable, from a different queue.
Here is some code to demonstrate my situation:
class SomeClass {
var commonResource: [String: String] = [:]
var queueA = DispatchQueue(label: "A")
var queueB = DispatchQueue(label: "B")
var schedulerB = ConcurrentDispatchQueueScheduler(queue: QueueB)
func writeToResourceInOnNext() {
let obs: PublishSubject<String> = OtherClass.GetObservable()
obs.observeOn(schedulerB)
.subscribe(onNext: { [weak self] res in
// this happens on queue B
self.commonResource["key"] = res
}
}
func writeToResource() {
// this happens on queue A
commonResource["key"] = "otherValue"
}
}
My question is, is it likely to have concurrency issues, if commonResource is modified in both places at the same time?
What is the common practice for writing/reading from class/global variables inside onNext in an observable with observeOn?
Thanks all!
Since your SomeClass has no control over when these functions will be called or on what threads the answer is yes, you are setup to have concurrency issues in this code due to its passive nature.
The obvious solution here is to dispatch to queue B inside writeToResource() in order to avoid the race condition.
Another option would be to use an NSLock (or NSRecursiveLock) and lock it before you write to the resource and unlock it after.
The best practice is: when you have a side effect happening inside a subscribe function's closure (in this case writing to commonResource that the closure is the only place where the side effect occurs. This would mean doing away with the passive writeToResource() function and instead passing in an Observable that was generated by whatever code currently is calling the function.
I know in general a publisher is more powerful than a closure, however I want to ask and discuss a specific example:
func getNotificationSettingsPublisher() -> AnyPublisher<UNNotificationSettings, Never> {
let notificationSettingsFuture = Future<UNNotificationSettings, Never> { (promise) in
UNUserNotificationCenter.current().getNotificationSettings { (settings) in
promise(.success(settings))
}
}
return notificationSettingsFuture.eraseToAnyPublisher()
}
I think this is a valid example of a Future publisher and it could be used here instead of using a completion handler. Let's do something with it:
func test() {
getNotificationSettingsPublisher().sink { (notificationSettings) in
// Do something here
}
}
This works, however it will tell me that the result of sink (AnyCancellable) is unused. So whenever I try to get a value, I need to either store the cancellable or assign it until I get a value.
Is there something like sinkOnce or an auto destroy of cancellables? Sometimes I don't need tasks to the cancelled. I could however do this:
func test() {
self.cancellable = getNotificationSettingsPublisher().sink { [weak self] (notificationSettings) in
self?.cancellable?.cancel()
self?.cancellable = nil
}
}
So once I receive a value, I cancel the subscription. (I could do the same in the completion closure of sink I guess).
What's the correct way of doing so? Because if I use a closure, it will be called as many times as the function is called, and if it is called only once, then I don't need to cancel anything.
Would you say normal completion handlers could be replaced by Combine and if so, how would you handle receiving one value and then cancelling?
Last but not least, the completion is called, do I still need to cancel the subscription? I at least need to update the cancellable and set it to nil right? I assume storing subscriptions in a set is for long running subscriptions, but what about single value subscriptions?
Thanks
Instead of using the .sink operator, you can use the Sink subscriber directly. That way you don't receive an AnyCancellable that you need to save. When the publisher completes the subscription, Combine cleans everything up.
func test() {
getNotificationSettingsPublisher()
.subscribe(Subscribers.Sink(
receiveCompletion: { _ in },
receiveValue: ({
print("value: \($0)")
})
))
}