Многопоточность — глубокое погружение в swift


Что такое параллелизм?

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

Зачем использовать параллелизм?

Очень важно, чтобы ваше приложение работало как можно более гладко и чтобы конечный пользователь никогда не был вынужден ждать, пока что-то произойдет. Секунда — это мизерный промежуток времени для большинства вещей, не связанных с компьютером. Однако если человеку приходится ждать секунду, чтобы увидеть ответ после выполнения действия на таком устройстве, как iPhone, это кажется вечностью. «Это слишком медленно» — один из основных факторов, по которым ваше приложение будет удалено.

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

GCD против операций

Существует два API, которые вы будете использовать при создании параллельного приложения: Grand Central Dispatch, обычно называемый GCD, и Operations. Это не конкурирующие технологии и не то, что вы должны выбирать между ними. На самом деле, Operations построены поверх GCD!

Grand Central Dispatch

GCD — это реализация Apple библиотеки libdispatch языка Си. Ее цель — поставить в очередь задачи — метод или закрытие — которые могут выполняться параллельно, в зависимости от доступности ресурсов; затем она выполняет задачи на доступном ядре процессора.

Хотя GCD использует потоки в своей реализации, вам, как разработчику, не нужно беспокоиться об их управлении. Задачи GCD настолько легки, что Apple в своем техническом обзоре GCD за 2009 год заявила, что для их реализации требуется всего 15 инструкций, в то время как для создания традиционных потоков может потребоваться несколько сотен инструкций.

Все задачи, которыми GCD управляет за вас, помещаются в очереди, управляемые GCD по принципу «первым пришел — первым ушел» (FIFO). Каждая задача, которую вы отправляете в очередь, затем выполняется в пуле потоков, полностью управляемых системой.

Более подробно о GCD рассказывается в этом блоге

// Class level variable
let queue = DispatchQueue(label: "sample")

// Somewhere in your function
queue.async {
  // Call slow non-UI methods here

  DispatchQueue.main.async {
    // Update the UI here
  }
}
Вход в полноэкранный режим Выход из полноэкранного режима

Синхронные и асинхронные задачи

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

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

Операции

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

Операция обычно отвечает за одну синхронную задачу. Это абстрактный класс, который никогда не используется напрямую. Вы можете использовать определенный системой подкласс 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. Функция 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!
Вход в полноэкранный режим Выход из полноэкранного режима

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

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

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

  • Готова: Она готова к запуску
  • Выполняется: Задание в настоящее время выполняется
  • Завершена: После завершения процесса
  • Отменено: Задание отмененоВажно знать, что операция может быть выполнена только один раз. Когда она находится в состоянии завершения или отмены, вы больше не можете перезапустить тот же самый экземпляр.

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..")
    }
}
Войти в полноэкранный режим Выход из полноэкранного режима

Преимущество GCD перед NSOperation:

i. реализация
Реализация GCD очень легковесна.
Операция сложна и тяжеловесна

Преимущества операции над GCD:

i. Контроль над операцией
вы можете приостановить, отменить, возобновить НСОперацию

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

iii. Состояние операции
вы можете отслеживать состояние операции или очереди операций: готовность, выполнение или завершение.

iv. Максимальное количество операций
вы можете указать максимальное количество операций в очереди, которые могут выполняться одновременно.

Состояние гонки

Состояние гонки возникает, когда два или более потока имеют доступ к общим данным и пытаются изменить их в одно и то же время. Поскольку алгоритм планирования потоков может менять потоки местами в любое время, вы не знаете, в каком порядке потоки будут пытаться получить доступ к общим данным. Поэтому результат изменения данных зависит от алгоритма планирования потоков, т.е. оба потока «гоняются» за доступом/изменением данных.

Диспетчерская группа

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

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

func doLongTasksAndWait () {
    print("starting long running tasks")
    let group = DispatchGroup()          //create a group for a bunch of tasks we are about to do
    for i in 0...3 {                     //launch a bunch of tasks (eg a bunch of webservice calls that all need to be finished before proceeding to the next ViewController)
        group.enter()                    //let the group know that something is being added
        DispatchQueue.global().async {   //run tasks on a background thread
            sleep(arc4random() % 4)      //do some long task eg webservice or database lookup (here we are just sleeping for a random amount of time for demonstration purposes)
            print("long task (i) done!")
            group.leave()                //let group know that the task is finished
        }
    }
    group.wait()                         //will block whatever thread we are on here until all the above tasks have finished (so maybe dont use this function on your main thread)
    print("all tasks done!")
}

//starting long running tasks
//long task 0 done!
//long task 3 done!
//long task 1 done!
//long task 2 done!
//all tasks done!
Вход в полноэкранный режим Выход из полноэкранного режима

Семафоры

Семафоры дают нам возможность контролировать доступ нескольких потоков к общему ресурсу.

Семафор состоит из очереди потоков и значения счетчика (тип Int).

Очередь потоков используется семафором для отслеживания ожидающих потоков в порядке FIFO (первый поток, внесенный в очередь, первым получит доступ к общему ресурсу, как только он станет доступен).

Значение счетчика используется семафором для принятия решения о том, должен ли поток получить доступ к общему ресурсу или нет. Значение счетчика изменяется, когда мы вызываем функцию signal() или wait().

Итак, когда мы должны вызывать функции wait() и signal()?

  • Вызывайте wait() каждый раз перед использованием общего ресурса. По сути, мы спрашиваем у семафора, доступен ли общий ресурс или нет. Если нет, мы будем ждать.
  • Вызывайте signal() каждый раз после использования общего ресурса. По сути, мы сигнализируем семафору, что закончили взаимодействие с общим ресурсом.

Вызов wait() сделает следующее:

Уменьшит счетчик семафора на 1.
Если полученное значение меньше нуля, поток замораживается.
Если полученное значение равно или больше нуля, код будет выполнен без ожидания.

Вызов signal() сделает следующее:

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

Пример

    let queue = DispatchQueue.global()
        let group = DispatchGroup()

        let sem = DispatchSemaphore(value: 1)

        queue.async(group: group){
            sem.wait()
            let movie = self.downloadMovie(name: "Avatar")
            self.movies.append(movie)
            sem.signal()
        }

        queue.async(group: group){
            sem.wait()
            self.saveMovie()
            self.movies.remove(at: 0)
            sem.signal()
        }

    func downloadMovie(name:String) -> String{
        sleep(4)
        print("Movie has been Downloaded")
        return name
    }

    func saveMovie(){
        sleep(2)
        print("Movie has been Saved")
    }

Вход в полноэкранный режим Выход из полноэкранного режима

Оцените статью
Procodings.ru
Добавить комментарий