Параллелизм в Swift с разделением ответственности

Операции в Swift - это мощный способ разделить обязанности между несколькими классами с одновременным отслеживанием прогресса и зависимостей. Они формально известны как NSOperations и используются в сочетании с OperationQueue.

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

Что такое операция в Swift?

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

Использование определенного системой BlockOperation выглядит следующим образом:

let blockOperation = BlockOperation {
    print("Executing!")
}

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

Это также можно сделать, добавив блок прямо в очередь:

queue.addOperation {
  print("Executing!")
}

Заданная задача добавляется в OperationQueue, и выполнение ее начинается при первой возможности.

Создание пользовательской операции

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

В следующем примере кода показан настраиваемый подкласс для импорта содержимого:

final class ContentImportOperation: Operation {

    let itemProvider: NSItemProvider

    init(itemProvider: NSItemProvider) {
        self.itemProvider = itemProvider
        super.init()
    }

    override func main() {
        guard !isCancelled else { return }
        print("Importing content..")
        
        // .. import the content using the item provider

    }
}

Класс берет поставщика элемента и импортирует содержимое в основной метод. Функция main() - единственный метод, который вам нужно перезаписать для синхронных операций. Добавьте операцию в очередь и установите блок завершения для отслеживания завершения:

let fileURL = URL(fileURLWithPath: "..")
let contentImportOperation = ContentImportOperation(itemProvider: NSItemProvider(contentsOf: fileURL)!)

contentImportOperation.completionBlock = {
    print("Importing completed!")
}

queue.addOperation(contentImportOperation)

// Prints:
// Importing content..
// Importing completed!

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

Различные состояния операции

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

  • Готово: все готово к запуску.
  • Выполняется: задача в настоящее время выполняется.
  • Готово: после завершения процесса.
  • Отменено: задача отменена.

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

В пользовательских реализациях вам необходимо вручную проверить отмененное состояние перед выполнением, чтобы убедиться, что задача отменяется. Знайте, что гонка данных может произойти, когда операция запускается и отменяется одновременно. Вы можете прочитать больше о гонках данных в моем сообщении в блоге Объяснение Thread Sanitizer: Гонки данных в Swift.

OperationQueue автоматически удалит задачу из очереди, как только она будет завершена, что произойдет как после выполнения, так и после отмены.

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

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

let fileURL = URL(fileURLWithPath: "..")
let contentImportOperation = ContentImportOperation(itemProvider: NSItemProvider(contentsOf: fileURL)!)
contentImportOperation.completionBlock = {
    print("Importing completed!")
}

let contentUploadOperation = UploadContentOperation()
contentUploadOperation.addDependency(contentImportOperation)
contentUploadOperation.completionBlock = {
    print("Uploading completed!")
}

queue.addOperations([contentImportOperation, contentUploadOperation], waitUntilFinished: true)

// Prints:
// Importing content..
// Uploading content..
// Importing completed!
// Uploading completed!

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

Вы должны реализовать проверку, чтобы увидеть, были ли отменены зависимости или нет:

final class UploadContentOperation: Operation {
    override func main() {
        guard !dependencies.contains(where: { $0.isCancelled }), !isCancelled else {
            return
        }

        print("Uploading content..")
    }
}

Заключение

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