异步操作可以与 OperationQueue 上的“progress”一起使用吗?

Can asynchronous operations be used with `progress` on OperationQueue?

从 iOS13 开始,可以使用 progress 属性 监控 OperationQueue 的进度。文档指出,在跟踪进度时,只有不覆盖 start() 的操作才算在内。但是,根据文档,异步操作 必须 覆盖 start() 而不是调用 super()

这是否意味着 asynchronous 操作和 progress 是互斥的(即只有同步操作可以用于进度)?如果是这种情况,这似乎是一个巨大的限制。

在我自己的项目中,我删除了对 start() 的覆盖,所有 看起来 都可以正常工作(例如,依赖项仅在设置 isFinished 时启动true 在我的异步操作基础 class 内部的依赖操作上。但是,这似乎有风险,因为 Operation 明确声明要覆盖 start().

想法?

参考文献:

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.

更新:我最终放弃了我的 AysncOperation,换成了一个简单的 SyncOperation,它一直等到 finish() 被调用(使用信号量)。

/// 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()
    }
}

您正在结合两个不同但相关的概念;异步和并发。

OperationQueue 总是将 Operations 分派到一个单独的线程上,因此您不需要显式地使它们成为异步的,也不需要重写 start()。在操作完成之前,您应该确保您的 main() 不会 return。如果您执行异步任务(例如网络操作),这意味着阻塞。

可以直接执行Operation。在您希望并发执行这些操作的情况下,您需要使它们异步。在这种情况下,您将覆盖 start()

If you want to implement a concurrent operation—that is, one that runs asynchronously with respect to the calling thread—you must write additional code to start the operation asynchronously. For example, you might spawn a separate thread, call an asynchronous system function, or do anything else to ensure that the start method starts the task and returns immediately and, in all likelihood, before the task is finished.

Most developers should never need to implement concurrent operation objects. If you always add your operations to an operation queue, you do not need to implement concurrent operations. When you submit a nonconcurrent operation to an operation queue, the queue itself creates a thread on which to run your operation. Thus, adding a nonconcurrent operation to an operation queue still results in the asynchronous execution of your operation object code. The ability to define concurrent operations is only necessary in cases where you need to execute the operation asynchronously without adding it to an operation queue.

总而言之,如果您想利用 progress

,请确保您的操作是同步的,并且不要覆盖 start

更新

虽然通常的建议是不要尝试使异步任务同步,但在这种情况下,如果您想利用 progress,这是您唯一可以做的事情。问题是如果你有一个异步操作,队列无法判断它何时真正完成。如果队列无法判断操作何时完成,则它无法为该操作准确更新 progress

您确实需要考虑这样做对线程池的影响。

另一种方法是不使用内置的 progress 功能并创建您自己的 属性 并从您的任务中更新。

你问:

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.

是的,如果你实现start,你必须自己将操作的childProgress添加到队列的parentprogress中。 (令人惊讶的是,他们没有通过观察 isFinished KVO 让基本操作更新进度,但事实就是如此。或者他们可以使用 becomeCurrent(withPendingUnitCount:)-resignCurrent模式,那么这种脆弱的行为就不会存在了。)

但我不会仅仅因为您想要它们 Progress 就放弃异步操作。通过使您的操作同步,您将在操作期间不必要地占用数量非常有限的工作线程之一。这是一种看起来非常方便的决定,可能不会立即带来问题,但 longer-term 可能会在您意外耗尽工作线程池时引入极其难以识别的问题。

幸运的是,添加我们自己的 child Progress 非常简单。考虑一个自定义操作,它有自己的 child Progress:

class TestOperation: AsynchronousOperation {
    let progress = Progress(totalUnitCount: 1)

    override func main() {
        DispatchQueue.main.asyncAfter(deadline: .now() + 1) { [self] in
            progress.completedUnitCount = 1
            finish()
        }
    }
}

然后,在将它们添加到您的队列时,将操作的 progress 添加为操作队列的 Progress:

的 child
class ViewController: UIViewController {
    @IBOutlet weak var progressView: UIProgressView!

    let queue: OperationQueue = ...

    override func viewDidLoad() {
        super.viewDidLoad()

        queue.progress.totalUnitCount = 10
        progressView.observedProgress = queue.progress

        for _ in 0 ..< 10 {
            queue.progress.becomeCurrent(withPendingUnitCount: 1)
            queue.addOperation(TestOperation())
            queue.progress.resignCurrent()
        }
    }
}

将您自己的自定义异步 Operation 子类的 Progress 添加到操作队列的 Progress 是微不足道的。或者,您可以创建自己的 parent Progress 并完全绕过 OperationQueueprogress。但无论哪种方式,它都非常简单,没有必要将婴儿(异步自定义 Operation 子类)连同洗澡水一起扔掉。


如果需要,您可以进一步简化调用点,例如,为 Progress:

的操作定义 typealias
typealias ProgressOperation = Operation & ProgressReporting

extension OperationQueue {
    func addOperation(progressOperation: ProgressOperation, pendingUnitCount: Int64 = 1) {
        progress.addChild(progressOperation.progress, withPendingUnitCount: pendingUnitCount)
        addOperation(progressOperation)
    }
}

class TestOperation: AsynchronousOperation, ProgressReporting {
    let progress = Progress(totalUnitCount: 1)

    override func main() { ... }
}

然后在添加操作时:

queue.progress.totalUnitCount = 10
progressView.observedProgress = queue.progress

for _ in 0 ..< 10 {
    queue.addOperation(progressOperation: TestOperation())
}