Starting in iOS13, one can monitor the progress of an OperationQueue
using the progress
property. The documentation states that only operations that do not override start()
count when tracking progress. However, asynchronous operations must override start()
and not call super()
according to the documentation.
Does this mean asynchronous
operations and progress
are mutually exclusive (i.e. only synchronous operations can be used with progress)? This seems like a massive limitation if this is the case.
In my own project, I removed my override of start()
and everything appears to work okay (e.g. dependencies are only started when isFinished
is set to true
on the dependent operation internally in my async operation base class). BUT, this seems risky since Operation
explicitly states to override start()
.
Thoughts?
Documentaiton references:
https://developer.apple.com/documentation/foundation/operationqueue/3172535-progress
By default, OperationQueue doesn’t report progress until totalUnitCount is set. When totalUnitCount is set, the queue begins reporting progress. Each operation in the queue contributes one unit of completion to the overall progress of the queue for operations that are finished by the end of main(). Operations that override start() and don’t invoke super don’t contribute to the queue’s progress.
https://developer.apple.com/documentation/foundation/operation/1416837-start
If you are implementing a concurrent operation, you must override this method and use it to initiate your operation. Your custom implementation must not call super at any time. In addition to configuring the execution environment for your task, your implementation of this method must also track the state of the operation and provide appropriate state transitions.
Update: I ended up ditching my AysncOperation
for a simple SyncOperation
that waits until finish()
is called (using a semaphore).
/// A synchronous operation that automatically waits until `finish()` is called.
open class SyncOperation: Operation {
private let waiter = DispatchSemaphore(value: 0)
/// Calls `work()` and waits until `finish()` is called.
public final override func main() {
work()
waiter.wait()
}
/// The work of the operation. Subclasses must override this function and call `finish()` when their work is done.
open func work() {
preconditionFailure("Subclasses must override `work()` and call `finish()`")
}
/// Finishes the operation.
///
/// The work of the operation must be completed when called. Failing to call `finish()` is a programmer error.
final public func finish() {
waiter.signal()
}
}
2
Answers
You are combining two different but related concepts; asynchronous and concurrency.
An
OperationQueue
always dispatchesOperations
onto a separate thread so you do not need to make them explicitly make them asynchronous and there is no need to overridestart()
. You should ensure that yourmain()
does not return until the operation is complete. This means blocking if you perform asynchronous tasks such as network operations.It is possible to execute an
Operation
directly. In the case where you want concurrent execution of those operations you need to make them asynchronous. It is in this situation that you would overridestart()
In summary, make sure your operations are synchronous and do not override
start
if you want to take advantage ofprogress
Update
While the normal advice is not to try and make asynchronous tasks synchronous, in this case it is the only thing you can do if you want to take advantage of
progress
. The problem is that if you have an asynchronous operation, the queue cannot tell when it is actually complete. If the queue can’t tell when an operation is complete then it can’t updateprogress
accurately for that operation.You do need to consider the impact on the thread pool of doing this.
The alternative is not to use the inbuilt
progress
feature and create your own property that you update from your tasks.You ask:
Yes, if you implement
start
, you have to add the operation’s childProgress
to the queue’s parentprogress
yourself. (It is a little surprising that they did not have the base operation update the progress by observing theisFinished
KVO, but it is what it is. Or they could have used thebecomeCurrent(withPendingUnitCount:)
–resignCurrent
pattern, and then this fragile behavior would not exist.)But I would not abandon asynchronous operations solely because you want their
Progress
. By making your operation synchronous, you would unnecessarily tie up one of the very limited number of worker threads for the duration of the operation. That is the sort of decision that seems very convenient, might not introduce immediate problems, but longer-term might introduce problems that are exceedingly hard to identify when you unexpectedly exhaust your worker thread pool.Fortunately, adding our own child
Progress
is exceedingly simple. Consider a custom operation with its own childProgress
:And then, while adding them to your queue, add the operation’s
progress
as a child of the operation queue’sProgress
:It is trivial to add the
Progress
of your own, custom, asynchronous,Operation
subclasses to the operation queue’sProgress
. Or, you might just create your own parentProgress
and bypass theprogress
of theOperationQueue
entirely. But either way, it is exceedingly simple and there is no point in throwing the baby (asynchronous customOperation
subclass) away with the bathwater.If you want, you could simplify the calling point even further, e.g., define
typealias
for operations withProgress
:And then when adding operations: