Можно ли использовать асинхронные операции с `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()
Если вы хотите реализовать параллельную операцию, то есть такую, которая выполняется асинхронно по отношению к вызывающему потоку, вы должны написать дополнительный код для асинхронного запуска операции. Например, вы можете создать отдельный поток, вызвать асинхронную системную функцию или сделать что-либо еще, чтобы гарантировать, что метод запуска запускает задачу и возвращает ее немедленно и, по всей вероятности, до завершения задачи.
Большинству разработчиков никогда не потребуется реализовывать объекты параллельных операций. Если вы всегда добавляете свои операции в очередь операций, вам не нужно реализовывать параллельные операции. Когда вы отправляете неконкурентную операцию в очередь операций, очередь сама создает поток, в котором выполняется ваша операция. Таким образом, добавление непараллельной операции в очередь операций по-прежнему приводит к асинхронному выполнению кода объекта операции. Возможность определять параллельные операции необходима только в тех случаях, когда вам нужно выполнить операцию асинхронно, не добавляя ее в очередь операций.
Таким образом, убедитесь, что ваши операции синхронны и не переопределяют
Обновлять
Хотя обычным советом является не пытаться сделать асинхронные задачи синхронными, в данном случае это единственное, что вы можете сделать, если хотите воспользоваться преимуществами . Проблема в том, что если у вас есть асинхронная операция, очередь не может сказать, когда она действительно завершена. Если очередь не может сказать, когда операция завершена, она не может точно обновляться для этой операции.
Вам нужно учитывать влияние этого на пул потоков.
Альтернатива - не использовать встроенный