Можно ли использовать асинхронные операции с `progress` в OperationQueue?

Начиная с iOS13, можно отслеживать ход выполнения OperationQueueиспользуя имущество. В документации указано, что при отслеживании прогресса учитываются только операции, которые не переопределяются. Однако асинхронные операции должны переопределять, а не вызывать super()согласно документации.

Это значит asynchronousоперации и progressявляются взаимоисключающими (т.е. с прогрессом можно использовать только синхронные операции)? Это кажется огромным ограничением, если это так.

В моем собственном проекте я удалил переопределение, и все работает нормально (например, зависимости запускаются только тогда, когда isFinishedустановлен на trueна зависимой операции внутри моего базового класса асинхронной операции). НО, это кажется рискованным, поскольку Operationявно указывает на переопределение start().

Мысли?

Ссылки на документы:

https://developer.apple.com/documentation/foundation/operationqueue/3172535-progress

По умолчанию OperationQueue не сообщает о ходе выполнения, пока не задано значение totalUnitCount. Когда установлено значение totalUnitCount, очередь начинает сообщать о ходе выполнения. Каждая операция в очереди добавляет одну единицу завершения к общему ходу очереди для операций, завершенных к концу main(). Операции, которые переопределяют start() и не вызывают super, не влияют на ход очереди.

https://developer.apple.com/documentation/foundation/operation/1416837-start

Если вы реализуете параллельную операцию, вы должны переопределить этот метод и использовать его для запуска операции. Ваша пользовательская реализация не должна вызывать super в любое время. Помимо настройки среды выполнения для вашей задачи, реализация этого метода также должна отслеживать состояние операции и обеспечивать соответствующие переходы между состояниями.

Обновление: в итоге я бросил свой 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()
    }
}

2 ответа

Ты спрашиваешь:

Означает ли это, что асинхронные операции и ход выполнения являются взаимоисключающими (т. е. с ходом выполнения можно использовать только синхронные операции)? Это кажется огромным ограничением, если это так.

Да, если реализовать start, вы должны самостоятельно добавить дочернюю операцию в родительскую очередь. (Немного удивительно, что базовая операция не обновляла прогресс, наблюдая за isFinishedКВО, но это то, что есть. Или они могли бы использовать becomeCurrent(withPendingUnitCount:)- resignCurrentпаттерн, и тогда этого хрупкого поведения не существовало бы.)

Но я бы не стал отказываться от асинхронных операций только потому, что вы хотите их . Делая свою операцию синхронной, вы без необходимости связываете один из очень ограниченного числа рабочих потоков на время операции. Такое решение кажется очень удобным, может не вызвать немедленных проблем, но в более долгосрочной перспективе может привести к проблемам, которые чрезвычайно трудно идентифицировать, когда вы неожиданно исчерпаете свой пул рабочих потоков.

К счастью, добавление нашего собственного потомка чрезвычайно просто. Рассмотрим пользовательскую операцию со своим дочерним элементом:

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

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

А затем, добавляя их в свою очередь, добавьте операцию как дочернюю часть очереди операций:

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

Тривиально добавить свои собственные, настраиваемые, асинхронные подклассы в очередь операций . Или вы можете просто создать свой собственный родитель и обойти progressпринадлежащий OperationQueueполностью. Но в любом случае это чрезвычайно просто и нет смысла бросать ребенка (асинхронный пользовательский Operationподкласс) прочь с водой.


Если вы хотите, вы можете еще больше упростить точку вызова, например, определить typealiasдля операций с Progress:

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

Вы комбинируете два разных, но родственных понятия; асинхронность и параллелизм.

Ан всегда отправляет в отдельный поток, поэтому вам не нужно делать их явно асинхронными, и нет необходимости переопределять . Вы должны убедиться, что ваш не возвращается, пока операция не будет завершена. Это означает блокировку, если вы выполняете асинхронные задачи, такие как сетевые операции.

Можно выполнить напрямую. В случае, когда вы хотите одновременное выполнение этих операций, вам нужно сделать их асинхронными. Именно в этой ситуации вы бы переопределили start()

Если вы хотите реализовать параллельную операцию, то есть такую, которая выполняется асинхронно по отношению к вызывающему потоку, вы должны написать дополнительный код для асинхронного запуска операции. Например, вы можете создать отдельный поток, вызвать асинхронную системную функцию или сделать что-либо еще, чтобы гарантировать, что метод запуска запускает задачу и возвращает ее немедленно и, по всей вероятности, до завершения задачи.

Большинству разработчиков никогда не потребуется реализовывать объекты параллельных операций. Если вы всегда добавляете свои операции в очередь операций, вам не нужно реализовывать параллельные операции. Когда вы отправляете неконкурентную операцию в очередь операций, очередь сама создает поток, в котором выполняется ваша операция. Таким образом, добавление непараллельной операции в очередь операций по-прежнему приводит к асинхронному выполнению кода объекта операции. Возможность определять параллельные операции необходима только в тех случаях, когда вам нужно выполнить операцию асинхронно, не добавляя ее в очередь операций.

Таким образом, убедитесь, что ваши операции синхронны и не переопределяют если вы хотите воспользоваться

Обновлять

Хотя обычным советом является не пытаться сделать асинхронные задачи синхронными, в данном случае это единственное, что вы можете сделать, если хотите воспользоваться преимуществами . Проблема в том, что если у вас есть асинхронная операция, очередь не может сказать, когда она действительно завершена. Если очередь не может сказать, когда операция завершена, она не может точно обновляться для этой операции.

Вам нужно учитывать влияние этого на пул потоков.

Альтернатива - не использовать встроенный функцию и создайте собственное свойство, которое вы обновляете из своих задач.

Другие вопросы по тегам