Многопоточность (concurrency) в Swift 3. GCD и Dispatch Queues

Надо сказать, что многопоточность (сoncurrency) в iOS всегда входит в вопросы, задаваемые на интервью разработчикам iOS приложений, а также в число топ ошибок, которые делают программисты при разработке iOS приложений. Поэтому так важно владеть этим инструментом в совершенстве.
Итак, у вас есть приложение, оно работает на main thread (главном потоке), который отвечает за выполнение кода, отображающего ваш пользовательский интерфейс (UI). Как только вы начинаете добавлять к вашему приложению такие «затратные по времени» куски кода, как загрузка данных из сети или обработка изображений на main thread (главном потоке), то работа вашего UI начинает сильно замедляться и даже может привести к полному его «замораживанию».
d6be2f372083452f922723c9c934e811.png

Как можно изменить архитектуру приложения, чтобы таких проблем не возникало? В этом случае на помощь приходит многопоточность (сoncurrency), которая позволяет одновременно выполнять две или более независимые задачи (tasks): вычисления, загрузку данных из сети или с диска, обработку изображений и т.д.

Процессор в каждый заданный момент времени может выполнять одину из ваших задач и для нее выделяется соответствующий поток (thread).
В случае одноядерного процессора (iPhone и iPad), многопоточность (сoncurrency) достигается многократными кратковременными переключениями между «потоками» (threads), на которых выполняются задачи (tasks), создавая достоверное представление об одновременном выполнении задач на одноядерном процессоре. На многоядерном процессоре (Mac) многопоточность достигается тем, что каждому «потоку», связанному с задачей, предоставляется свое собственное ядро для запуска задач. Обе эти технологии используют общее понятие многопоточности (сoncurrency).

Своеобразной платой за введение многопоточности в вашем приложениии является трудность обеспечения безопасного выполнения кода на различных потоках (thread safety). Как только мы позволяем задачам (tasks) работать параллельно, появляются проблемы, связанные с тем, что разные задачи (tasks) захотят получить доступ к одним и тем же ресурсам, например, захотят изменять одну и ту же переменную в разных потоках, или захотят получить доступ к ресурсам, которые уже заблокированы другими задачами. Это может привести к разрушению самих ресурсов, используемых задачами на других потоках.

В iOS программировании многопоточность предоставляется разработчикам в виде нескольких инструментов: Thread, Grand Central Dispatch (сокращенно GCD) и Operation — и используется с целью увеличения производительности и отзывчивости пользовательского интерфейса. Мы не будем рассматривать Thread, так как это низкоуровневый механизм, а сосредоточимся на GCD в этой статье и Operation (объектно-ориентированном API, построенном поверх GCD) в дальнейшей публикации.
Надо сказать, что до появления Swift 3 столь мощный фреймворк, как Grand Central Dispatch (GCD), имел API, основанное на языке С, которое на первый взгляд кажется просто книгой заклинаний, и не сразу понятно, как мобилизовать его возможности для выполнения полезных пользовательских задач.
В Swift 3 все кардинально изменилось. GCD получил новый, полностью Swift-подобный синтаксис, который очень легко использовать. Если вы хотя бы немного знакомы со старым API GCD, то весь новый синтаксис покажется вам просто легкой прогулкой; если нет — то вам просто придется изучить еще один обычный раздел программирования на iOS. Новый фреймворк GCD работает в Swift 3 на всех Apple устройствах, начиная от Apple Watch, включая все iOS приборы, и кончая Apple TV и Mac.
Еще одна хорошая новость состоит в том, что начиная с Xcode 8, можно использовать для изучения GCD и Operation такой мощный и наглядный инструмент, как Playgroud. В Xcode 8 появился новый вспомогательный класс PlaygroudPage, у которого есть функция, позволяющая Playgroud жить неограниченное время. В этом случае очередь DispatchQueue может работать до тех пор, пока работа не закончится. Это особенно важно для сетевых запросов. Для того, чтобы использовать класс PlaygroudPage, вам нужно импортировать модуль PlaygroudSupport. Этот модуль также позволяет получить доступ к циклу выполнения (run loop), отображать «живой» UI, а также выполнять асинхронные операции на Playgroud. Ниже мы увидим, как выглядит эта настройка в работе. Эта новая возможность Playground в Xcode 8 делает изучение многопоточности в Swift 3 очень простым и наглядным.

Для лучшего понимания многопоточности (concurrency), Apple ввела некоторые абстрактные понятия, с которыми оперируют оба инструмента — GCD и Operation. Основным понятием является очередь (queue). Поэтому, когда мы говорим о многопоточности в iOS с точки зрения разработчика iOS приложений, мы говорим об очередях (queues). Очереди (queues) — это обычные очереди, в которые выстраиваются люди, чтобы купить, например, билет в кинотеатр, но в нашем случае в очередь выстраиваются замыкания (closure  — анонимные блоки кода). Система просто выполняет их согласно очереди, «выдергивая» следующего по очереди и запуская его на выполнение в соответствующем этой очереди потоке. Очереди (queues) следуют FIFO паттерну (First In, First Out), это означает, что тот, кто первым был поставлен в очередь, будет первым направлен на выполнение. У вас может быть множество очередей (queues) и система «выдергивает» замыкания по одному из каждой очереди и запускает их на выполнение в их собственных потоках. Таким образом, вы получаете многопоточность.

Но это лишь общее представление о том, как многопоточность (сoncurrency) работает в iOS. Интрига заключается в том, что собой представляют эти очереди в смысле выполнения заданий по отношению друг к другу (последовательное или параллельное) и с помощью какой функции (синхронной или асинхронной) эти задания помещаются в очередь, тем самым блокируя или не блокируя текущую очередь.

Последовательные (serial) и параллельные (concurrent) очереди.
Очереди (queues) могут быть »serial» (последовательными), когда задача (замыкание), которая находится на вершине очереди, «вытягивается» iOS и работает до тех пор, пока не закончится, затем вытягивается следующий элемент из очереди и т.д. Это serial queue или последовательная очередь. Очереди (queues) могут быть «concurrent» (многопоточными), когда система «вытягивает» замыкание, находящееся на вершине очереди, и запускает ее на выполнение в определенном потоке. Если у системы еще есть ресурсы, то она берет следующий элемент из очереди и запускает его на выполнение в другом потоке в то время, пока первая функция еще работает. И так система может вытянуть целый ряд функций. Для того, чтобы не путать общее понятие многопоточности с "concurrent queues" (многопоточными очередями), мы будем называть "concurrent queue" параллельной очередью, имея ввиду порядок выполнения заданий на ней по отношению друг к другу, не вдаваясь в техническую реализацию этой параллельности.
cba064d50117484a92217b48d556cafd.png

Мы видим, что на serial (последовательной) очереди завершение замыканий происходит строго в том порядке, в каком они поступали на выполнение, в то время как на concurrent (параллельной) очереди задания заканчиваются непредсказуемым образом. Кроме того, вы видите, что общее время выполнения определенной группы заданий на serial очереди значительно превосходит время выполнения той же группы заданий на concurrent очереди. На serial (последовательной) очереди в любой текущий момент времени выполняется только одно задание, а на concurrent(параллельной) очереди число заданий в любой текущий момент времени может меняться.

Синхронное и асинхронное выполнение заданий.

Как только очередь (queue) создана, задание на ней можно разместить с помощью двух функций: sync — синхронное выполнение по отношению к текущей очереди и async — асинхронное выполнение по отношению к текущей очереди.

Синхронная функция sync возвращает управление на текущую очередь только после полного завершения задания, тем самым блокируя текущую очередь:

49fda33d32be4a6ebb511bf0530a8854.png

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

c32c597b60d24ae69f5fffe4ca155d9c.png

«Другой очередью» может оказаться в случае асинхронного выполнения как последовательная (serial) очередь:

cfc0fb700a034acbab2cc25fc6e14f17.png

так и параллельная (concurrent) очередь:

3919fb1ff81747dca78feabb45e3a9de.png

Задача разработчика состоит только в выборе очереди и добавлении задания (как правило, замыкания) в эту очередь синхронно с помощью функции sync или асинхронно с помощью функции async, дальше работает исключительно iOS.

Возвращаясь к задаче, представленной в самом начале этого поста, мы переключим выполнение задания получения данных из сети «Data from Network» на другую очередь:

07ba5295c1b540fb8fa2c0bb3d8da835.png

После получения данных Data из сети на другой очереди Dispatch Queue, мы посылаем их обратно на Main thread.

d3e85e84a6034f58aa0b4ae8d977d3c6.png

Когда мы получаем данные Data из сети на другой очереди DispatchQueue, Main thread — свободна и обслуживает все события, которые происходят на UI. Давайте посмотрим, как выглядит реальный код для этого случая:

3c263f1c570a4c35a4d22e56dbabf264.png

Для выполнения загрузки данных по URL-адресу imageURL, что может занять значительное время и заблокировать Main queue, мы АСИНХРОННО переключаем выполнение этого ресурса-емкого задания на глобальную параллельную очередь с качеством обслуживания qos, равным .utility (более подробно об этом чуть позже):

  let imageURL: URL = URL(string: "http://www.planetware.com/photos-large/F/france-paris-eiffel-tower.jpg")!
    let queue = DispatchQueue.global(qos: .utility)
    queue.async{
        if let data = try? Data(contentsOf: imageURL){
            DispatchQueue.main.async {
                image.image = UIImage(data: data)
                 print("Show image data")
            }
            print("Did download  image data")
        }
    }

После получения данных data мы вновь возвращаемся на Main queue, чтобы обновить наш UI элемент image1.image с помощью этих данных.
Вы видите, как просто выполнить цепочку переключений на другую очередь, чтобы «увести» выполнение «затратных» заданий с Main queue, а затем опять на нее вернуться. Код находится на EnvironmentPlayground.playground на Github.

Заметьте, что переключение затратных заданий с Main queue на другой поток всегда АСИНХРОННО.
Нужно быть очень внимательным с методом sync для очередей, потому что «текущий поток» вынужден ждать окончания выполнения задания на другой очереди. НИКОГДА НЕ вызывайте метод sync на Main queue, потому что это приведет к deadlock вашего приложения! (об этом ниже)

Глобальные очереди.

Помимо пользовательских очередей, которые нужно специально создавать, система iOS предоставляет в распоряжение разработчика готовые (out-of-the-box) глобальные очереди (queues). Их 5:

1.) последовательная очередь Main queue, в которой происходят все операции с пользовательским интерфейсом (UI):

let main = DispatchQueue.main

Если вы хотите выполнить функцию или замыкание, которые что-то делают с пользовательским интерфейсом (UI), с UIButton или с UI-чем-нибудь, вы должны поместить эту функцию или замыкание на Main queue. Эта очередь имеет наивысший приоритет среди глобальных очередей.

2.) 4 фоновых concurrent (параллельных) глобальных очереди с разным качеством обслуживания qos и, конечно, разными приоритетами:

// наивысший приоритет
let userInteractiveQueue = DispatchQueue.global(qos: .userInteractive)

let userInitiatedQueue = DispatchQueue.global(qos: .userInitiated)

let utilityQueue = DispatchQueue.global(qos: .utility)

// самый низкий приоритет
let backgroundQueue = DispatchQueue.global(.background) 

// по умолчанию 
let defaultQueue = DispatchQueue.global()

Каждую из этих очередей Apple наградила абстрактным «качеством обслуживания» qos (сокращение для Quality of Service), и мы должны решить, каким оно должно быть для наших заданий.

Ниже представлены различные qos и объясняется, для чего они предназначены:

  • .userInteractive — для заданий, которые взаимодействуют с пользователем в данный момент и занимают очень мало времени: анимация, выполняются мгновенно; пользователь не хочет этого делать на Main queue, однако это должно быть сделано по возможности быстро, так как пользователь взаимодействует со мной прямо сейчас. Можно представить ситуацию, когда пользователь водит пальцем по экрану, а вам необходимо просчитать что-то, связанное с интенсивной обработкой изображения, и вы размещаете расчет в этой очереди. Пользователь продолжает водить пальцем по экрану, он не сразу видит результат, результат немного отстает от положения пальца на экране, так как расчеты требуют некоторого времени, но по крайней мере Main queue все еще «слушает» наши пальцы и реагирует на них. Эта очередь имеет очень высокий приоритет, но ниже, чем у Main queue.
  • .userInitiated — для заданий, которые инициируются пользователем и требуют обратной связи, но это не внутри интерактивного события, пользователь ждет обратной связи, чтобы продолжить взаимодействие; может занять несколько секунд; имеет высокий приоритет, но ниже, чем у предыдущей очереди,
  • .utulity — для заданий, которые требуют некоторого времени для выполнения и не требуют немедленной обратной связи, например, загрузка данных или очистка некоторой базы данных. Делается что-то, о чем пользователь не просит, но это необходимо для данного приложения. Задание может занять от несколько секунд до нескольких минут; приоритет ниже, чем у предыдущей очереди,
  • .background — для заданий, не связанных с визуализацией и не критичных ко времени исполнения; например, backups или синхронизация с web  — сервисом. Это то, что обычно запускается в фоновом режиме, происходит только тогда, когда никто не хочет никакого обслуживания. Просто фоновая задача, которая занимает значительное время от минут до часов; имеет наиболее низкий приоритет среди всех глобальных очередей.

Есть еще Глобальная параллельная (concurrency) очередь по умолчанию .default, которая сообщает об отсутствие информации о «качестве обслуживания» qos. Она создается с помощью оператора:
DispatchQueue.global()

Если удается определить qos информацию из других источников, то используется она, если нет, то используется qos между .userInitiated и .utility.

Важно понимать, что все эти глобальные очереди являются СИСТЕМНЫМИ глобальными очередями и наши задания — не единственные задания в этой очереди! Также важно знать, что все глобальные очереди, кроме одной, являются concurrent (параллельными) очередями.

Особенная Глобальная последовательная очередь для пользовательского интерфейса — Main queue.

Apple обеспечивает нас единственной ГЛОБАЛЬНОЙ serial (ПОСЛЕДОВАТЕЛЬНОЙ) очередью — это упомянутая выше Main queue. На этой очереди нежелательно выполнять ресурсо-емкие операции (например, загрузку данных из сети), не относящиеся с изменению UI, чтобы не «замораживать» UI на время выполнения этой операции и сохранить отзывчивость пользовательского интерфейса на действия пользователя в любой момент времени, например, на жесты.

947064a217b2454a8b1b629eaad4057c.png

Настоятельно рекомендуется «уводить» такие ресурсо-емкие операции на другие потоки или очереди:

253e8a99b42c46aba8c17af901618fc7.png

Есть и еще одно жесткое требование — ТОЛЬКО на Main queue мы можем изменять UI элементы.

Это потому, что мы хотим, чтобы Main queue была не только «отзывчивой» на действия с UI (да, это основная причина), но мы хотим также, чтобы пользовательский интерфейс был защищен от «разлаживания» в многопоточной среде, то есть реакция на действия пользователя выполнялась бы строго последовательно в упорядоченной манере. Если мы разрешим нашим элементам UI выполнять свои действия в различных очередях, то может случиться, что рисование будет происходить с разной скоростью, и действия будет пересекаться, что приведет к полной непредсказуемости на экране. Мы используем Main queue как своего рода «точку синхронизации», в которую возвращается каждый, кто хочет «рисовать» на экране.

Проблемы многопоточности.

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

  • cостояние гонки (race condition) — ошибка проектирования многопоточной системы или приложения, при которой работа системы или приложения зависит от того, в каком порядке выполняются части кода
  • инверсия приоритетов (priority inversion)
  • взаимная блокировка (deadlock) — ситуация в многопоточной системе, при которой несколько потоков находятся в состоянии бесконечного ожидания ресурсов, занятых самими этими потоками

Состояние гонки (race condition).

Мы можем воспроизвести простейший случай race condition, если будем изменять переменную value асинхронно на private очереди, а показывать value на текущем потоке:

62551afc7c57436eaeab7a4a67dfc9b2.png

У нас есть обычная переменная value и обычная функция changeValue для ее изменения, причем умышленно мы сделали с помощью оператора sleep(1) так, что изменение переменной value требует значительного времени. Если мы будем запускать функцию changeValue АСИНХРОННО с помощью async, то прежде, чем дойдет дело до размещения измененного значения в переменной value, на текущем потоке переменная value может быть переустановлена в другое значение, это и есть race condition. Этому коду соответствует печать в виде:

588996d0319646c4ad766285485fa0ea.png

и диаграмма, на которой наглядно видно явление под названием »race condition»:

4e9ddb5deb494edabfc975da717f7958.png

Давайте заменим метод async на sync:

75e6085ea469425395705e93e0bf3bf6.png

И печать, и результат изменились:

94087163acb44a83906be79f57ba44fb.png

и диаграмма, на которой отсутствует явление под названием »race condition»:

cfe4872d84724dc6a5d6dea169957456.png

Мы видим, что хотя нужно быть очень внимательным с методом sync для очередей, потому что «текущий поток» вынужден ждать окончания выполнения задания на другой очереди, метод sync оказывается очень полезным для того, чтобы избежать race conditions. Код для имитации явления »race condition» можно посмотреть на firstPlayground.playground на Github. Позже мы покажем настоящие »race condition» при формировании строки из символов, получаемых на разных потоках. Будет также предложен элегантный способ формирования строки с использованием «барьеров», который позволит избежать»race conditions» и сделать формируемую строку потокобезопасной.

Инверсия приоритетов (priority inversion).

С блокировкой ресурсов тесно связано понятие инверсии приоритетов:

2a40815749274c3897e581524cc2f539.png

Допустим в системе существуют две задачи с низким (А) и высоким (Б) приоритетом. В момент времени T1 задача (А) блокирует ресурс и начинает его обслуживать. В момент времени T2 задача (Б) вытесняет низкоприоритетную задачу (А) и пытается завладеть ресурсом в момент времени T3. Но так как ресурс заблокирован, задача (Б) переводится в ожидание, а задача (А) продолжает выполнение. В момент времени Т4 задача (А) завершает обслуживание ресурса и разблокирует его. Так как ресурс ожидает задача (Б), она тут же начинает выполнение.
Временной промежуток (T4-T3) называют ограниченной инверсией приоритетов. В этом промежутке наблюдается логическое несоответствие с правилами планирования — задача с более высоким приоритетом находится в ожидании в то время как низкоприоритетная задача выполняется.

Но это еще не самое страшное. Допустим в системе работают три задачи: низкоприоритетная (А), со средними приоритетом (Б) и высокоприоритетная (В):

0110446fbda54b8aa59534af9b0a8f78.png

Если ресурс заблокирован задачей (А), а он требуется задаче (В), то наблюдается та же ситуация — высокоприоритетная задача блокируется. Но допустим, что задача (Б) вытеснила (А), после того как (В) ушла в ожидание ресурса. Задача (Б) ничего не знает о конфликте, поэтому может выполняться сколь угодно долго на промежутке времени (T5-T4). Кроме того, помимо (Б) в системе могут быть и другие задачи, с приоритетами больше (А), но меньше (Б). Поэтому длительность периода (T6-T3) в общем случае неопределена. Такую ситуацию называют неограниченной инверсией приоритетов.

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

Ниже мы покажем, как можно с помощью DispatchWorkItem объектов увеличивать приоритет отдельных заданий на текущей очереди.

Взаимная блокировка (deadlock).

Взаимная блокировка — это аварийное состояние системы, которое может возникать при вложенности блокировок ресурсов. Допустим в системе существуют две задачи с низким (А) и высоким (Б) приоритетом, которые используют два ресурса — X и Y:

9d83a98445554b0486486c287d07b16d.png

В момент времени T1 задача (А) блокирует ресурс X. Затем в момент времени T2 задачу (А) вытесняет более приоритетная задача (Б), которая в момент времени T3 блокирует ресурс Y. Если задача (Б) попытается заблокировать ресурс X (T4) не освободив ресурс Y, то она будет переведена в состояние ожидания, а выполнение задачи (А) будет продолжено. Если в момент времени T5 задача (А) попытается заблокировать ресурс Y, не освободив X, возникнет состояние взаимной блокировки — ни одна из задач (А) и (Б) не сможет получить управление.

Взаимная блокировка возможна только тогда, когда в системе используется зависимый (вложенный) многопоточный доступ к ресурсам. Взаимной блокировки можно избежать, если не использовать вложенность, или если ресурс использует протокол увеличения приоритета.
Если мы в задаче, представленной в начале поста, после получения данных из сети в фоновой очереди, попытаемся использовать для возвращения на main queue метод sync, то мы мы получим взаимную блокировку (deadock).

НИКОГДА НЕ вызывайте метод sync на main queue, потому что это приведет к взаимной блокировке (deadlock) вашего приложения!

Экспериментальная среда.

Для экспериментов мы будем использовать Playground, настроенную на бесконечное время работы c помощью модуль PlaygroundSupport и класса PlaygroudPage, чтобы мы смогли завершиться все задачи, помещенные в очереди и получить доступ к main queue. Мы можем остановить ожидание какого-то события на Playground c помощью команды PlaygroundPage.current.finishExecution().
Есть еще одна крутая возможность на Playground — возможность взаимодействия с «живым» UI с помощью команды

PlaygroundPage.liveView = viewController

и Ассистента Редактора (Assistant Editor). Если, например, вы создаете viewController, то для того, чтобы увидеть ваш viewController, вам достаточно настроить Playground на неограниченное выполнение кода и включить Ассистента Редактора (Assistant Editor). Придется закомментировать команду PlaygroundPage.current.finishExecution() и останавливать Playground вручную.

4df034dfc92c49aeafe1b0865d2ee172.png

Playground c кодом шаблона экспериментальной среды имеет имя EnvironmentPlayground.playground и находится на Github.

1. Первый эксперимент. Глобальные очереди и задания.

Начнем с простых экспериментов. Определим также ряд глобальных очередей: одну последовательную mainQueue — это main queue, и четыре параллельные (concurrent) queues — userInteractiveQueue, userQueue, utilityQueue и backgroundQueue. Можно задать concurrent queue по умолчанию — defautQueue:

7c2c0458682a4b6195365c69f9abae0f.png

В качестве задания task выберем печать любых десяти одинаковых символов и приоритета текущей очереди. Еще одно задание taskHIGH, которое будет печатать один символ, мы будем запускать с высоким приоритетом:

eeba8822320940a4b55ae0547d9cdcdb.png

2. Второй эксперимент будет касаться СИНХРОННОСТИ и АСИНХРОННОСТИ на глобальных очередях.

Как только вы получили глобальную очередь, например, userQueue, вы можете выполнять задания на ней либо СИНХРОННО, используя метод sync, либо АСИНХРОННО, используя метод async.

1bebd59937b946f0994e23de7e096359.png

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

В случае же асинхронного async выполнения, мы видим, что задания
1cc369457f1c4804b1c13f653a404e75.png
стартуют, не дожидаясь завершения заданий
01a499a9300e49d6ada90f665225560d.png
, и приоритет глобальной очереди userQueue выше приоритета выполнения кода на Playground. Следовательно, задания на userQueue выполняются чаще.

3. Третий эксперимент. Private последовательные очереди.

Помимо глобальный очередей мы можем создавать пользовательские Private очереди с помощью инициализатора класса DispatchQueue:

4159320735f842c2bab1a2d3dd346772.png

Единственное, что необходимо указать при создании пользовательской очереди, — это уникальная метка label, которую Apple рекомендует задавать в виде инверсной DNS нотации ("com.bestkora.mySerial”), именно под таким именем будет видна эта очередь в отладчике. Тем не менее, это необязательно, и вы можете использовать любую строку, лишь бы она оставалась уникальной. Если вы не задаете больше никаких других аргументов кроме label при инициализации Private очереди, то по умолчанию создается последовательная (.serial) очередь. Есть и другие аргументы, которые можно задать при инициализации очереди, и о них мы поговорим чуть позже.
Смотрим, как работает пользовательская Private последовательная очередь mySerialQueue при использовании sync и async методов:

6339f81cd8f34041a240082f0545db47.png

В случае синхронного sync мы видим ту же ситуацию, что и в эксперименте 3 -тип очереди не имеет значения, потому что в качестве оптимизации функция sync может запустить замыкание на текущем потоке. Именно это мы и видим.

Что произойдет, если мы используем async метод и позволим последовательной очереди mySerialQueue выполнить задания
01a499a9300e49d6ada90f665225560d.png
асинхронно по отношению к текущей очереди? В этом случае выполнение программы не останавливается и не ожидает, пока завершится это задание в очереди mySerialQueue; управление немедленно перейдет к выполнению заданий
1cc369457f1c4804b1c13f653a404e75.png
и будет исполнять их в одно и то же время, что и задания
01a499a9300e49d6ada90f665225560d.png

4. Четвертый эксперимент будет касаться приоритетов QoS последовательных очередей.

Давайте назначим нашей Private последовательной очереди serialPriorityQueue качество обслуживания qos, равное .userInitiated, и поставим асинхронно в эту очередь сначала задания
01a499a9300e49d6ada90f665225560d.png
, а потом
1cc369457f1c4804b1c13f653a404e75.png
Этот эксперимент убедит нас в том, что наша новая очередь serialPriorityQueue действительно является последовательной, и несмотря на использование async метода, задания выполняются последовательно друг за другом в порядке поступления:

0abfbc7c06ff46aca33529e91cdee03c.png

Таким образом, для многопоточного выполнения кода недостаточно использовать метод async, нужно иметь много потоков либо за счет разных очередей, либо за счет того, что сама очередь является параллельной (.concurrent). Ниже в эксперименте 5 с параллельными (.concurrent) очередями мы увидим аналогичный эксперимент с Private параллельной (.concurrent) очередью workerQueue, но там будет совсем другая картина, когда мы будем помещать в эту очередь те же самые задания.

Давайте используем последовательные Private очереди с разными приоритетами для асинхронной постановки в эту очереди сначала заданий
01a499a9300e49d6ada90f665225560d.png
, а потом заданий
1cc369457f1c4804b1c13f653a404e75.png

очередь serialPriorityQueue1 c qos .userInitiated
очередь serialPriorityQueue2 c qos .background

3a945e00a4d34d3d8d93d4dba5678ad9.png

Здесь происходит многопоточное выполнение заданий, и задания чаще исполняются на очереди serialPriorityQueue1, имеющей более приоритетное качество обслуживания qos: .userIniatated.

Вы можете задержать выполнение заданий на любой очереди DispatchQueue на заданное время, например, на now() + 0.1 с помощью функции asyncAfter и еще изменить при этом качество обслуживания qos:

5d1c4613699e4c3a92c7025ad92abce5.png

5. Пятый эксперимент будет касаться Private параллельных (concurrent) очередей.

Для того, чтобы инициализировать Private параллельную (.concurrent) очередь достаточно указать при инициализации Private очереди значение аргумента attributes равное .concurrent. Если вы не указываете этот аргумент, то Private очередь будет последовательной (.serial). Аргумент qos также не требуется и может быть пропущен без всяких проблем.

Давайте назначим нашей параллельной очереди workerQueue качество обслуживания qos, равное .userInitiated, и поставим асинхронно в эту очередь сначала задания
01a499a9300e49d6ada90f665225560d.png
, а потом
1cc369457f1c4804b1c13f653a404e75.png
Наша новая параллельная очередь workerQueue действительно является параллельной, и задания в ней выполняются одновременно, хотя все, что мы сделали по сравнению со четвертым экспериментом (одна последовательная очередь serialPriorityQueue), это задали аргумент attributes равном .concurrent:

a7c8ef1377424a3b8484f5f9cb62daf1.png

Картина совершенно другая по сравнению с одной последовательной очередью. Если там все задания выполняются строго в том порядке, в котором они поступают на выполнение, то для нашей параллельной (многопоточной) очереди workerQueue, которая может «расщепляться» на несколько потоков, задания действительно выполняются параллельно: некоторые задания с символом
442d3428bf8744dfb4054f328f5c9d63.png
, будучи позже поставлены в очередь workerQueue, выполняются быстрее на параллельном потоке.

Давайте используем параллельные Private очереди с разными приоритетами:

очередь workerQueue1 c qos .userInitiated
очередь workerQueue2 c qos .background

ae94b8e9f74847efb5b85c4291013e2a.png

Здесь такая же картина, как и с разными последовательными Private очередями во втором эксперименте. Мы видим, что задания чаще исполняются на очереди workerQueue1, имеющей более высокий приоритет.

Можно создавать очереди с отложенным выполнением с помощью аргумента attributes, а затем активировать выполнение заданий на ней в любое подходящее время c помощью метод

© Habrahabr.ru