I'm creating an NSOperation that executes a closure with a delay. The operations are added into a queue, every time before a new operation is added I cancel all existing ones in the queue:
let myOp = SomeOperation { [weak self] in /* do something */ }
queue.cancelAllOperations()
queue.addOperation(myOp)
Operation Code 1
final class SomeOperation: Operation {
private let closure: () -> Void
init(closure: @escaping () -> Void) {
self.closure = closure
}
override func main() {
if isCancelled {
return
}
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3, execute: doSomething)
}
private func doSomething() {
guard isCancelled == false else {
return
}
closure()
}
}
While the above code works, the code below doesn't. In the DispatchQueue closure, self is nil:
Operation Code 2
final class SomeOperation: Operation {
private let closure: () -> Void
init(closure: @escaping () -> Void) {
self.closure = closure
}
override func main() {
if isCancelled {
return
}
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { [weak self] in
guard let self = self else { return }
guard isCancelled == false else { return }
self.closure()
}
}
}
So I'm trying to learn a bit deeper:
- In Code 2,
selfisnilbecause as soon asDispatchQueue.main.asyncAfter…is called, themainmethod finishes and the operation is thus released. - Code 1 works because
execute: doSomethingimplicitly captures/retains aself, so even afterasyncAfter,selfis still there.
So my questions are:
- In Apple's doc it says for concurrent operations I should rather be using
start,asynchronous,executing,finished, etc. In my case I just need to have a delay, not actually doing anything asynchronous, should I do it inmainonly or should I do it as an async operation by implementing those methods Apple suggested? - Is my thinking correct that in Code 1 there's a
selfretained implicitly, which doesn't sound correct and can create retain cycle?
Thanks!
You asked:
First, you are doing something asynchronous. I.e.,
asyncAfteris asynchronous.Second, the motivating reason behind Apple’s concurrent operation discussion is that the operation should not finish until the asynchronous task it launched also finishes. You talk about canceling the operation, but that only makes sense if the operation is still running by the time you go to cancel it. This feature, the wrapping the asynchronous task in an object while never blocking a thread, is one of the key reasons we use operations rather than just GCD. It opens the door for all sorts of elegant dependencies between asynchronous tasks (dependencies, cancelation, etc.).
Regarding the strong reference cycle issues, let’s look at your first example. While it is prudent for the creator of the operation to use
[weak self]capture list, it should not be required. Good design of the operation (or anything using asynchronously called closures) is to have it release the closure when it is no longer needed:It doesn’t mean that the caller shouldn’t use
[weak self]capture list, only that the operation no longer requires it, and will resolve any strong reference cycles when it is done with the closure.[Note, in the above, I omitted synchronization of the variable, to keep it simple. But you need to synchronize your access to it to ensure thread-safe design.]
But this design begs the question as to why would you would want to keep the
asyncAfterscheduled, still firing even after you canceled the operation. It would be better to cancel it, by wrapping the closure in aDispatchWorkItem, which can be canceled, e.g.:Having outlined the memory issues, we should note that this is all probably moot, as you probably should just make this a concurrent operation (with all that custom KVO) as you identified in the documentation. Besides, all this care we’ve put into cancelation logic only applies if the operation is alive until the asynchronous process finishes. So, we will make a concurrent operation. E.g.,
The above uses an asynchronous operation base class that (a) performs the necessary KVO notifications; and (b) is thread-safe. Here is one random example of how that could be implemented: