Добро пожаловать! Если вы уже читали предыдущие статьи из этой серии, то наверняка знакомы с многопоточностью в iOS. Мы освоили концепции потоков (Thread/pthread) и научились управлять синхронной и асинхронной обработкой задач в очередях с помощью фреймворка Dispatch. Однако на этом инструменты обеспечения многопоточности не заканчиваются. В этой части мы погрузимся в мир операций, изучим их особенности и поймем, в каких случаях целесообразно прибегать к классу Operation, а когда достаточно функциональности, предоставляемой GCD.

  1. Про многопоточность 1. Thread

  2. Про многопоточность 2. GCD

  3. Про многопоточность 3. Operation

  4. Про многопоточность 4. Async/await (coming soon)

  5. Про многопоточность 5. Железяки (coming soon)

Содержание

  1. Вступление

  2. Operation

  3. BlockOperation

  4. OperationQueue

  5. Operation vs GCD

  6. Заключение

Вступление

Operation — это один из наиболее высокоуровневых инструментов для работы с многопоточностью в iOS. Это мощное API, входящее в состав фреймворка Foundation. По сравнению с GCD, Operation предоставляет более гибкие и настраиваемые средства управления потоками выполнения. Однако для оптимального использования этого инструмента необходимо понимание его особенностей и принципов работы. Давайте в этой статье ближе познакомимся с Operation и рассмотрим, в каких случаях его применение может оказаться наиболее полезным, а также какие преимущества и нюансы он имеет по сравнению с уже изученными нами методами.

Operation

Operation - это мощный инструмент для управления многопоточностью, предоставляемый фреймворком Foundation. Суть Operation заключается в абстракции задачи, которую необходимо выполнить. Он предоставляет не только удобный способ управления жизненным циклом задачи, но и доступ к её состоянию.

Как абстрактный класс, Operation не применяется напрямую, а используется путем создания собственных подклассов операций или использования предоставленных фреймворком Foundation подклассов, таких как BlockOperation (или objc реалзиация NSInvocationOperation).

???? Одной из аналогий для понимания Operation может быть паттерн проектирования Command, где задача инкапсулируется в объекте и может быть выполнена по запросу.

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

Чтобы операция выполнила свою задачу, её можно добавить в OperationQueue — специальную очередь для операций. OperationQueue эффективно распределяет выполнение операций на вторичных потоках, используя под капотом библиотеку GCD.

Помимо этого, операцию можно запустить непосредственно, вызвав метод start() у объекта Operation. Это может быть полезно, например, для выполнения операции в текущем потоке.

Отличительной чертой Operation является возможность отменить операцию даже во время её выполнения, что отличает ее от DispatchWorkItem в GCD.

Жизненный цикл Operation

Operation предоставляет жизненный цикл, который позволяет контролировать и управлять выполнением задач. Давайте подробно рассмотрим состояния и методы этого цикла:

Operation lifecycle.jpg

Состояния Operation:

  • Pending – операция запланирована для выполнения в OperationQueue или ждет выполнения зависимых задач. В этом состоянии операция может быть отменена.

  • Ready – операция готова к выполнению. Она переходит в это состояние, когда все зависимые операции выполнены или когда операция ожидает своей очереди в OperationQueue. Операцию также можно отменить в этом состоянии.

  • Executing – операция выполняется. В этом состоянии она все еще может быть отменена.

  • Finished – операция выполнена.

  • Cancelled – операция была отменена или уничтожена.

Свойства, связанные с состоянием:

  • isReady – определяет готовность операции к выполнению или нахождение в состоянии Pending.

  • isAsynchronous – указывает, является ли операция асинхронной. По умолчанию операция синхронна, но это свойство можно настроить для асинхронного поведения.

  • isExecuting – определяет, находится ли операция в состоянии Executing, то есть в процессе выполнения.

  • isFinished – указывает, что операция завершила свое выполнение.

  • isCancelled – определяет, была ли операция отменена.

Основные методы:

  • main() – абстрактный метод, переопределение которого позволяет определить основную функциональность операции.

  • start() – запускает выполнение операции.

Создание и управление операцией

Как уже упоминалось ранее, Operation является абстрактным классом. Для создания собственной операции нам нужно унаследовать класс Operation и реализовать основные методы и свойства. Давайте рассмотрим простейший пример операции:

class CalculateOperation: Operation {
    private let a: Int
    private let b: Int
    var onCalc: ((_ sum: Int) -> Void)?

    init(a: Int, b: Int) {
        self.a = a
        self.b = b
    }

    override func main() {
        onCalc?(a + b)
    }
}

Как мы видим, для создания операции достаточно переопределить метод main() и реализовать в нем необходимую функциональность. Метод start() автоматически управляет состоянием операции и вызывает main():

let calculate = CalculateOperation(a: 2, b: 2)

calculate.onCalc = { sum in print(sum) }
calculate.start()

Важно отметить, что операция, запущенная методом start() без использования OperationQueue, будет выполняться синхронно на текущем потоке. Мы можем добавить поддержку асинхронности операции, воспользовавшись библиотекой GCD. Давайте добавим асинхронное поведение в нашу операцию:

class CalculateOperation: Operation {
    private let a: Int
    private let b: Int
    var onCalc: ((_ sum: Int) -> Void)?
    
    private var _isFinished: Bool = false
    private var _isExecuting: Bool = false
    
    private let queue = DispatchQueue(label: "calculate-operation.serial-queue")
    
    override var isFinished: Bool { _isFinished }
    override var isExecuting: Bool { _isExecuting }
    override var isAsynchronous: Bool { true }
    
    init(a: Int, b: Int) {
        self.a = a
        self.b = b
    }
    
    override func main() {
        onCalc?(a + b)
    
        _isExecuting = false
        _isFinished = true
    }
    
    override func start() {
        _isExecuting = true
    
        queue.async { self.main() }
    }

}

Переопределив метод start(), мы обеспечили асинхронное выполнение операции в очереди queue. В дополнение к этому, переопределив свойство isAsynchronous, мы возвращаем управление вызывающей очереди. Поскольку мы переопределили метод start(), теперь нам нужно самостоятельно управлять состоянием операции. Для этого достаточно переопределить свойства isExecuting и isFinished и изменить их значение в соответствии с текущим состоянием операции.

Одна из ключевых особенностей класса Operation - это поддержка KVO (Key-Value Observing), которая позволяет нам следить за изменением состояния операции. В приведенном выше примере мы не поддержали KVO для свойств isFinished и isExecuting, исправим это:

class CalculateOperation: Operation {
    // ...
    
    override func main() {
        onCalc?(a + b)
    
        willChangeValue(forKey: "isExecuting")
        _isExecuting = false
        didChangeValue(forKey: "isExecuting")
    
        willChangeValue(forKey: "isFinished")
        _isFinished = true
        didChangeValue(forKey: "isFinished")
    }
    
    override func start() {
        willChangeValue(forKey: "isExecuting")
        _isExecuting = true
        didChangeValue(forKey: "isExecuting")
    
        queue.async { self.main() }
    }
}

Поддержка KVO

Давайте наглядно проследим за изменением состояния Operation. Для этого воспользуемся классом-помощником, который будет наблюдать за изменением состояния операции, используя KVO:

import Foundation
// Описываем класс Observer, который сообщит нам текущее состояние переданной операции
final class OperationObserver: NSObject {
    // Описываем перечисление, которое будет определять состояние операции
    enum OperationState: String, CaseIterable {
        case asynchronous = "isAsynchronous"
        case ready = "isReady"
        case executing = "isExecuting"
        case finished = "isFinished"
        case cancelled = "isCancelled"
    }
    
    private let operation: Operation
    
    init(operation: Operation) {
        self.operation = operation
        super.init()
    
        // Подписываемся на изменения, используя KVO
        func observe() {
            OperationState
                .allCases
                .forEach {
                    operation.addObserver(
                        self,
                        forKeyPath: $0.rawValue,
                        options: .initial,
                        context: nil
                    )
                }
        }
    
        observe()
    }
    
    // Обрабатываем изменение состояния оперции. В нашем лучае выводим в консоль текущее значение свойства.
    override func observeValue(
        forKeyPath keyPath: String?,
        of object: Any?,
        change: [NSKeyValueChangeKey : Any]?,
        context: UnsafeMutableRawPointer?)
    {
        guard
            let keyPath = keyPath,
            let operationState = OperationState(rawValue: keyPath)
        else { return }
    
        func printOperationState(_ stateValue: Bool) { print("\(operationState.rawValue): \(stateValue)") }
    
        switch operationState {
        case .asynchronous: printOperationState(operation.isAsynchronous)
        case .ready:        printOperationState(operation.isReady)
        case .executing:    printOperationState(operation.isExecuting)
        case .finished:     printOperationState(operation.isFinished)
        case .cancelled:    printOperationState(operation.isCancelled)
        }
    }

}

Этот класс будет отслеживать изменение состояния операции и выводить его в консоль.

Теперь воспользуемся уже созданной ранее операцией, запустим ее выполнение и проследим за изменением состояния:

let calculate = CalculateOperation(a: 2, b: 2)
calculate.onCalc = { sum in print(sum) }

let observer = OperationObserver(operation: calculate)
calculate.start()

Взглянем на вывод консоли. Так как в качестве аргумента options метода addObserver мы передали значение .initial, то в первую очередь мы увидим начальное значение состояния операции:

isAsynchronous: true
isReady: true
isExecuting: false
isFinished: false
isCancelled: false

Во вторую очередь мы увидим изменение состояния после начала выполнения задачи операции:

isExecuting: true
4
isFinished: true
isExecuting: false

Стоит отметить, что состояние isReady может быть изменено только один раз и только в том случае, если у данной операции есть зависимость, выполнение которой переводит операцию в состояние Pending, а свойство isReady в false.

???? Свойство isAsynchronous необходимо переопределять только в том случае, если мы будем взаимодействовать с операцией без использования OperationQueue. OperationQueue игнорирует свойство isAsynchronous и самостоятельно вызывает метод start() на отдельном потоке.

Зависимости

У каждой операции есть возможность добавить в качестве зависимости другую операцию. Данная возможность позволяет задавать порядок выполнения зависимых операций. Для добавления и удаления зависимостей существуют методы:

func addDependency(_ op: Operation)
func removeDependency(_ op: Operation)

Когда мы запускаем выполнение операции, сначала выполнятся все зависимые операции в порядке их добавления и только после этого выполнится основная операция. Операция, имеющая зависимости, будет пребывать в состоянии Pending до тех пор, пока последняя зависимая операция не будет выполнена и только после этого перейдет в состояние Ready (это означает, что когда зависимые операции начнут свое выполнение, свойство isReady приобретет значение false). Отмена зависимой операций не прервет цепочку выполнения остальных операций, отменная операция в данном случае будет считаться завершенной.

Рассмотрим простейший пример использования зависимостей:

let operation1 = BlockOperation {
    print("Operation 1 is complete")
}

let operation2 = BlockOperation {
    print("Operation 2 is complete")
}
operation2.addDependency(operation1)

let queue = OperationQueue()
queue.addOperations([operation1, operation2], waitUntilFinished: true)

В этом примере operation1 будет выполнена перед operation2, так как operation2 зависит от неё.

Обработка ошибок и откат операций

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

Прежде всего, вы можете определить собственные ошибки для вашей операции, создав структуру или перечисление ошибок. Далее, в методе main() операции, вы можете проверять наличие ошибок и, если они обнаружены, вызвать метод cancel() операции.

enum OperationError: Error, LocalizedError {
    case failedToExecute
    var errorDescription: String? {
        switch self {
        case .failedToExecute: return "Failed to execute"
        }
    }
}

class MyOperation: Operation {
    var condition: Bool = false
    var onError: ((_ error: OperationError) -> Void)?
    
    override func main() {
        if condition {
            // Возникла ошибка, отменяем выполнение операции
            cancel(withError: .failedToExecute)
            return
        }
    
        // Выполнение операции
    }
    
    private func cancel(withError error: OperationError) {
        cancel()
        onError?(error)
    }
}

let operation = MyOperation()
operation.condition = false
operation.onError = { error in print(error) }

operation.start()

Для реализации отката операции к предыдущему состоянию, вам может потребоваться хранить некоторую информацию о состоянии операции перед ее выполнением. При возникновении ошибки, вы можете использовать эту информацию для восстановления состояния.

class MyOperation: Operation {

    // ...
    private var previousState: SomeState
  
    init(initialState: SomeState) {
        previousState = initialState
    }
  
    override func main() {
        do {
            // Выполнение операции
        } catch {
            // Возникла ошибка, восстанавливаем состояние
            restoreState(previousState)
            cancel(withError: error)
        }
    }
    
    private func restoreState(_ state: SomeState) {
        // Логика восстановления состояния
    }
    
	// ...
}

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

Обработка завершения операций

После того как операция завершит свою работу, может возникнуть необходимость выполнить какие-либо действия. Для этого в классе Operation предоставляются механизмы обработки завершения, такие как использование замыканий, KVO (Key-Value Observing) и блоков завершения.

Использование замыканий

При использовании BlockOperation, вы можете использовать замыкание, чтобы выполнить задачу после завершения операции. Для этого используется свойство completionBlock:

let operation = BlockOperation {
    // Выполнение операции
}

operation.completionBlock = {
    // Код, который выполнится после завершения операции
}

Замыкание, назначенное в completionBlock, будет вызвано сразу после завершения операции, независимо от того, успешно ли она выполнена или была отменена.

Блоки завершения

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

class CustomOperation: Operation {
    var onCompletion: (() -> Void)?
  
    override func main() {
        // Выполнение операции
        onCompletion?()
    }
}

let operation = CustomOperation()
operation.onCompletion = {
    // Действия после завершения операции
}

let queue = OperationQueue()
queue.addOperation(operation)

Операции с таймаутами

В некоторых сценариях выполнение операции может занять длительное время, и иногда вам может понадобиться ограничить время выполнения операции таймаутом. Это может быть полезно, чтобы избежать блокировки приложения из-за долгой операции. Хотя класс Operation сам по себе не предоставляет встроенного механизма для установки таймаутов, вы можете достичь этой функциональности, используя сочетание операций, очередей и GCD:

let timeoutInterval: TimeInterval = 10

let operation = BlockOperation {
    // Долгая операция
}

let semaphore = DispatchSemaphore(value: 0)

DispatchQueue.global().async {
    operation.start()
    semaphore.signal()
}

if semaphore.wait(timeout: .now() + timeoutInterval) == .timedOut {
    operation.cancel()
    print("Operation timed out")
} else {
    print("Operation completed successfully")
}

В этом примере мы используем DispatchSemaphore, чтобы ожидать завершения операции с заданным таймаутом. Если операция завершится до истечения таймаута, мы получим успешный результат. В противном случае, если операция не завершится вовремя, мы вызовем метод cancel() для отмены операции и запринтим сообщение о таймауте.

BlockOperation

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

Создание BlockOperation простое и интуитивно понятное. Вы можете создать экземпляр BlockOperation, передав задачу на выполнение прямо в инициализатор:

let operation = BlockOperation {
    print("Hello, World!")
}

Задача, которую вы передаете в BlockOperation, будет выполнена асинхронно, когда операция будет запущена.

Один из уникальных аспектов BlockOperation заключается в том, что вы можете добавить несколько блоков кода, которые будут выполнены в рамках одной операции. Это можно сделать с помощью метода addExecutionBlock(_:):

let operation = BlockOperation {
    print("Hello, World!")
}

operation.addExecutionBlock {
    print("Hello, again!")
}

operation.start()

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

Как и все операции, BlockOperation можно запустить, вызвав метод start(). Однако на практике обычно используется OperationQueue для управления запуском операций.

Если вам нужно отменить операцию, вы можете вызвать метод cancel(). Однако обратите внимание, что это не гарантирует немедленное прекращение выполнения задач. Вместо этого необходимо периодически проверять состояние isCancelled операции и завершать выполнение, если это состояние станет true.

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

let operationQueue = OperationQueue()

let operation = BlockOperation {
    print("Hello, World!")
}

operationQueue.addOperation(operation)

Как и все операции, BlockOperation поддерживает зависимости и приоритеты выполнения. Вы можете сделать одну операцию зависимой от другой, используя метод addDependency(_:), и вы можете управлять порядком выполнения операций с помощью свойства queuePriority.

OperationQueue

OperationQueue представляет собой инструмент для управления коллекцией операций (Operation). Этот высокоуровневый API обеспечивает удобный способ организации и координации выполнения асинхронных задач.

Введение

OperationQueue предоставляет среду выполнения для операций, позволяя эффективно распараллеливать задачи. Операции добавляются в очередь с помощью метода addOperation(_:):

let operationQueue = OperationQueue()

let operation = MyOperation()

operationQueue.addOperation(operation)

Зависимости

OperationQueue позволяет определить зависимости между операциями, контролируя их порядок выполнения. Зависимости устанавливаются с использованием метода addDependency(_:):

let operation1 = MyOperation()

let operation2 = MyOperation()
operation2.addDependency(operation1)

operationQueue.addOperation(operation1)
operationQueue.addOperation(operation2)

Установка зависимости: operation2 зависит от operation1. Это гарантирует, что operation2 начнет выполнение только после завершения operation1.

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

Управление количеством операций

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

let operationQueue = OperationQueue()

operationQueue.maxConcurrentOperationCount = 2

Управление состоянием

Вы можете приостанавливать и возобновлять выполнение операций с помощью методов suspend() и resume(). Метод cancelAllOperations() отменяет все операции в очереди. Метод waitUntilAllOperationsAreFinished() блокирует текущий поток выполнения, пока все операции в очереди не завершатся.

Используя KVO (Key-Value Observing), можно наблюдать за состоянием OperationQueue и его операций. Это позволяет следить за такими свойствами, как количество операций в очереди (operationCount) и состояние приостановки (isSuspended).

Пауза, возобновление и отложенный старт операций

По умолчанию операции, добавленные в очередь, начинают выполнение как только это становится возможным. Однако иногда требуется отложить начало выполнения операции или приостановить уже работающую очередь. Для этого в классе OperationQueue предоставляется свойство isSuspended.

Пример использования:

let operationQueue = OperationQueue()

let operation1 = BlockOperation {
    print("Operation 1 is complete")
}

let operation2 = BlockOperation {
    print("Operation 2 is complete")
}

// Приостановка выполнения очереди
operationQueue.isSuspended = true
operationQueue.addOperation(operation1)
operationQueue.addOperation(operation2)

// Операции начнут выполнение только после снятия приостановки
operationQueue.isSuspended = false

В этом примере, когда isSuspended установлено в true, операции operation1 и operation2 не начнут выполнение. Они будут ожидать до тех пор, пока isSuspended не будет установлено в false. Таким образом, вы можете контролировать момент начала выполнения операций в очереди.

Приоритет выполнения и очередность операций

При работе с Operation и OperationQueue у вас есть возможность управлять приоритетом выполнения операций. Приоритет операции определяется с использованием двух свойств: queuePriority и qualityOfService. Эти свойства позволяют вам указать, как важна операция относительно других операций в очереди и в общем контексте системы.

  • queuePriority: свойство позволяет задать приоритет операции внутри конкретной очереди. Возможные значения: .veryLow, .low, .normal, .high, .veryHigh. Операции с более высоким приоритетом будут выполнены раньше операций с более низким приоритетом внутри одной очереди.

  • qualityOfService: свойство определяет общий приоритет выполнения операции в системе. Оно встречается нам как в pthread, так и в GCD. Возможные значения: .background, .utility, .default, .userInitiated, .userInteractive. Этот приоритет влияет на выполнение операций на уровне операционной системы и может повлиять на распределение ресурсов между операциями в разных приложениях.

Пример использования:

let operationQueue = OperationQueue()

let operation1 = BlockOperation {
    print("Operation 1 is complete")
}
operation1.queuePriority = .high

let operation2 = BlockOperation {
    print("Operation 2 is complete")
}
operation2.queuePriority = .low

operationQueue.addOperation(operation1)
operationQueue.addOperation(operation2)

В данном примере operation1 будет выполнена раньше operation2, так как у нее приоритет выше. Однако, важно понимать, несмотря на то, что приоритет операций влияет на их порядок выполнения внутри одной очереди, он не гарантирует выполнение в абсолютно определенном порядке, так как существует множество факторов, также играющих роль.

Группировка операций

Группировка операций позволяет объединять несколько операций вместе и выполнить какую-либо дополнительную работу, когда все операции в группе завершатся. Это может быть полезным, например, когда вам нужно подготовить данные или изменить состояние перед выполнением следующей последовательности операций.

Для группировки операций в OperationQueue вы можете использовать класс BlockOperation для создания операции, в которой выполнится код, ожидающий завершения определенных операций.

Пример использования группировки операций:

let operationQueue = OperationQueue()

let operation1 = BlockOperation {
    print("Operation 1 is complete")
}

let operation2 = BlockOperation {
    print("Operation 2 is complete")
}

let groupOperation = BlockOperation {
    print("All operations are complete")
}

groupOperation.addDependency(operation1)
groupOperation.addDependency(operation2)

operationQueue.addOperations([operation1, operation2, groupOperation], waitUntilFinished: false)

В этом примере создаются три операции: operation1, operation2 и groupOperation. Операция groupOperation добавляет зависимости от operation1 и operation2, что означает, что она будет ждать их завершения перед выполнением. Таким образом, код, указанный в блоке операции groupOperation, выполнится только после завершения всех операций в группе.

Как вы можете видеть в примере, специфичного API для создания группы в Operation и OperationQueue не существует, так как эта функциональность уже поддерживается простой конструкцией добавления зависимостей.

Отмена операций

Отмена операции осуществляется методом cancel(), который изменяет состояние операции на "отменено". Важно заметить, что cancel() не останавливает немедленно выполняющуюся операцию, но предоставляет ей сигнал о необходимости завершения. Операции должны периодически проверять свое состояние и завершаться при обнаружении отмены.

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

Пример использования отмены операций с зависимостями:

let operationQueue = OperationQueue()

let operation1 = BlockOperation {
    print("Operation 1 is complete")
}

let operation2 = BlockOperation {
    print("Operation 2 is complete")
}
operation2.addDependency(operation1)

operationQueue.addOperations([operation1, operation2], waitUntilFinished: false)

// Отмена operation2
operation2.cancel()

// Проверка статусов отмены для operation1 и operation2
print("Operation 1 isCancelled: \(operation1.isCancelled)") // false
print("Operation 2 isCancelled: \(operation2.isCancelled)") // true

В этом примере операция operation2 зависит от операции operation1. Если вы вызовете метод cancel() для operation2, то это не приведет к автоматической отмене operation1 из-за их зависимости. Если вам необходимо отменить обе операции, необходимо вызвать метод cancel() у обеих.

Отслеживание прогресса

Класс Operation не предоставляет встроенного механизма для отслеживания прогресса выполнения операции, хотя это может оказаться особенно полезно, когда операция выполняется долго и вы хотите предоставить пользователю информацию о текущем состоянии операции.

Прогресс операции может отслеживается с использованием объекта типа Progress. Вы можете установить общее количество работы, которое операция должна выполнить, и обновлять прогресс по мере продвижения выполнения.

Пример использования:

class MyOperation: Operation {
  
    private let totalWorkUnits: Int64
    @objc dynamic lazy var progress = Progress(totalUnitCount: totalWorkUnits)
    
    init(totalWorkUnits: Int64) {
        self.totalWorkUnits = totalWorkUnits
        super.init()
    }
    
    override func main() {
        
        var completedWorkUnits: Int64 = 0
    
        // Проверка на отмену операции
        if isCancelled {
            return
        }
    
        // Асинхронное выполнение работы
        DispatchQueue.global().async { [weak self] in
            guard let self else { return }
            
            while completedWorkUnits < self.totalWorkUnits {
                
                // Проверка на отмену операции
                if self.isCancelled {
                    return
                }
    
                // Выполнение работы
                // ...
    
                completedWorkUnits += 1
                self.progress.completedUnitCount = completedWorkUnits
            }
        }
    }
}

// Создание экземпляров очереди и операции
let operationQueue = OperationQueue()

let operation = MyOperation(totalWorkUnits: 10)

// Подписка на изменения прогресса
let progressObserver = operation.progress.observe(\.fractionCompleted) { progress, _ in
    print("Progress: \(progress.fractionCompleted)")
}

operationQueue.addOperation(operation)

В этом примере мы создаем операцию MyOperation, в которой выполняется какая-то работа. Также мы создаем объект Progress, указывая суммарное количество выполненной работы. По мере выполнения операции, мы обновляем прогресс, что позволяет наблюдателю реагировать на изменения и отображать текущий прогресс выполнения.

Также прогресс выполнения можно отслеживать у очереди операции, используя свойство progress. OperationQueue не будет обновлять прогресс выполнения операций, до тех пор, пока не будет определено значение totalUnitCount. Прогресс очереди определяется количеством выполненных задач относительно значения totalUnitCount.

Потокобезопасность OperationQueue

Вы можете безопасно использовать один объект OperationQueue из нескольких потоков без создания дополнительных мьютексов для синхронизации доступа к этому объекту.

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

Потокобезопасность достигается благодаря использованию фреймворка Dispatch (GCD) под капотом OperationQueue для управления выполнением операций. Очереди операций оптимально используют доступные системные ресурсы, чтобы обеспечить параллельное выполнение операций, минимизируя блокировку главного потока или других задач.

Синхронизация ресурсов

В многопоточной среде, такой как использование операций и очередей, иногда возникает необходимость обеспечения безопасности и синхронизации доступа к ресурсам. Синхронизация ресурсов позволяет избежать состояний гонки, при которых несколько потоков пытаются изменять одни и те же данные одновременно. В классе Operation и его наследниках также можно применять механизмы синхронизации для обеспечения правильного доступа к данным.

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

class SharedResource {
    private var value = 0
    private let accessSemaphore = DispatchSemaphore(value: 1)
  
    func increment() {
        accessSemaphore.wait()
        value += 1
        accessSemaphore.signal()
    }

    func getValue() -> Int {
        defer {
            accessSemaphore.signal()
        }
      
        accessSemaphore.wait()
        return value
    }
}

let sharedResource = SharedResource()

let operation1 = BlockOperation {
    sharedResource.increment()
}

let operation2 = BlockOperation {
    let value = sharedResource.getValue()
    print("Value: \(value)")
}

let queue = OperationQueue()
queue.addOperations([operation1, operation2], waitUntilFinished: true)

Для достижения атомарности можно воспользоваться множеством инструментов из фреймворка Dispatch, семафор лишь один из возможных.

Operation vs GCD

Grand Central Dispatch (GCD) и классы операций (Operation и OperationQueue) представляют собой два разных подхода к управлению многопоточностью и асинхронностью. Оба подхода имеют свои преимущества и недостатки, и выбор между ними зависит от конкретных требований задачи.

Уровень абстракции

Одной из основных разниц между GCD и классами операций является уровень абстракции. GCD предоставляет более низкоуровневые механизмы для работы с потоками и очередями, такие как DispatchQueue, DispatchGroup, DispatchSemaphore и тд. Это позволяет более тонко настраивать поведение и управление потоками, но также требует больше внимания к деталям и потенциальным проблемам многопоточки.

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

Управление зависимостями

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

Управление жизненным циклом

Классы операций предоставляют более прозрачное управление жизненным циклом, чем GCD прямо из коробки. В GCD можно добиться подобной функциональности, но это также потребует больших трудозатрат.

Производительность и оптимизация

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

Вывод

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

Заключение

В этой статье мы рассмотрели ключевые аспекты работы с операциями и очередями операций. Сравнив классы операций с Grand Central Dispatch, мы обнаружили, что оба подхода имеют свои преимущества и могут быть выбраны в зависимости от требований конкретной задачи. Самая банальная, но в то же время уместная отсылочка для дедов нашего дела: “Серебрянной пули нет”.

В этой статье не были затронуты примеры решений популярных проблем многопоточности, просто потому что они решаются через все те же инструменты, что и в GCD или pthread. ¯\(ツ)

На мой взгляд, Operation и OperationQueue — это неотъемлемая часть арсенала iOS разработчика, которая может оказаться крайне полезной в конкретных задачах, хотя инструмент и не самый популярный.

Надеюсь статья оказалась полезной и помогла решить соответствующие проблемы. Спасибо за внимание! ????

Комментарии (0)