Во-первых, важно знать разницу между потоками и очередями и то, что на самом деле делает GCD. Когда мы используем очереди отправки (через GCD), мы действительно ставим в очередь, а не распределяем потоки. Инфраструктура Dispatch была разработана специально для того, чтобы увести нас от многопоточности, поскольку Apple признает, что «реализация правильного решения для многопоточности [может] стать чрезвычайно трудным, а иногда и невозможным». Следовательно, для одновременного выполнения задач (задач, которые мы не хотим, чтобы пользовательский интерфейс замораживался), все, что нам нужно сделать, это создать очередь этих задач и передать ее GCD. И GCD обрабатывает все связанные потоки. Поэтому все, что мы на самом деле делаем, - это выстраиваемся в очередь.
Второе, что нужно сразу знать, - что это за задача. Задача - это весь код в этом блоке очереди (не в очереди, потому что мы можем добавлять вещи в очередь все время, но в пределах закрытия, где мы добавили его в очередь). Задачу иногда называют блоком, а блок - задачей (но они чаще называются задачами, особенно в сообществе Swift). И независимо от того, сколько или мало кода, весь код в фигурных скобках считается одной задачей:
serialQueue.async {
// this is one task
// it can be any number of lines with any number of methods
}
serialQueue.async {
// this is another task added to the same queue
// this queue now has two tasks
}
И очевидно, что упоминание о том, что «одновременный» означает «одновременно с другими», а «последовательный» означает одно за другим (никогда в одно и то же время). Сериализовать что-либо или поместить что-то в последовательный формат, означает просто выполнить это от начала до конца в порядке слева направо, сверху вниз, без прерывания.
Есть два типа очередей: последовательные и параллельные, но все очереди параллельны друг относительно друга . Тот факт, что вы хотите запускать любой код «в фоновом режиме», означает, что вы хотите запускать его одновременно с другим потоком (обычно с основным потоком). Следовательно, все очереди отправки, последовательные или параллельные, выполняют свои задачи одновременно по отношению к другим очередям . Любая сериализация, выполняемая очередями (последовательными очередями), имеет отношение только к задачам в этой единственной [последовательной] очереди отправки (как в приведенном выше примере, где две задачи находятся в одной последовательной очереди; эти задачи будут выполняться одна за другой). другой, никогда одновременно).
ПОСЛЕДОВАТЕЛЬНЫЕ Очереди (часто известные как частные очереди отправки) гарантируют выполнение задач по одной от начала до конца в том порядке, в котором они были добавлены в эту конкретную очередь. Это единственная гарантия сериализации при обсуждении очередей отправки.- что определенные задачи в определенной последовательной очереди выполняются последовательно. Однако последовательные очереди могут выполняться одновременно с другими последовательными очередями, если они являются отдельными очередями, потому что, опять же, все очереди параллельны друг относительно друга. Все задачи выполняются в разных потоках, но не каждая задача гарантированно запускается в одном потоке (не важно, но интересно знать). И в iOS-фреймворке нет готовых последовательных очередей, вы должны их создать. Частные (неглобальные) очереди по умолчанию являются последовательными, поэтому для создания последовательной очереди:
let serialQueue = DispatchQueue(label: "serial")
Вы можете сделать его параллельным через его свойство attribute:
let concurrentQueue = DispatchQueue(label: "concurrent", attributes: [.concurrent])
Но на этом этапе, если вы не добавляете какие-либо другие атрибуты в частную очередь, Apple рекомендует вам просто использовать одну из их готовых к работе глобальных очередей (которые работают одновременно). Внизу этого ответа вы увидите другой способ создания последовательных очередей (с использованием свойства target), который Apple рекомендует делать (для более эффективного управления ресурсами). Но пока достаточно маркировки.
CONCURRENT QUEUES (часто называемые глобальными очередями отправки) могут выполнять задачи одновременно; Тем не менее, задачи гарантированно запускаются в том порядке, в котором они были добавлены в эту конкретную очередь, но, в отличие от последовательных очередей, очередь не ждет завершения первой задачи перед запуском второй задачи. Задачи (как и в случае с последовательными очередями) выполняются в разных потоках, и (как и в случае с последовательными очередями) не каждая задача гарантированно запускается в одном потоке (не важно, но интересно знать). Платформа iOS поставляется с четырьмя готовыми к использованию параллельными очередями. Вы можете создать параллельную очередь, используя приведенный выше пример или одну из глобальных очередей Apple (что обычно рекомендуется):
let concurrentQueue = DispatchQueue.global(qos: .default)
СОХРАНЕНИЕ ЦИКЛОВ: Очереди отправки являются объектами с подсчетом ссылок, но вам не нужно сохранять и освобождать глобальные очереди, потому что они являются глобальными и, таким образом, сохранение и освобождение игнорируются. Вы можете получить доступ к глобальным очередям напрямую, не назначая их свойству.
Есть два способа диспетчеризации очередей: синхронно и асинхронно.
SYNC DISPATCHING означает, что поток, в котором была отправлена очередь (вызывающий поток), приостанавливается после отправки очереди и ожидает завершения выполнения задачи в этом блоке очереди перед возобновлением. Для синхронной отправки:
DispatchQueue.global(qos: .default).sync {
// task goes in here
}
ASYNC DISPATCHING означает, что вызывающий поток продолжает работать после отправки очереди и не ждет, пока задача в этом блоке очереди завершит выполнение. Для асинхронной отправки:
DispatchQueue.global(qos: .default).async {
// task goes in here
}
Теперь можно подумать, что для последовательного выполнения задачи следует использовать последовательную очередь, а это не совсем так. Для последовательного выполнения нескольких задач следует использовать последовательную очередь, но все задачи (изолированные сами по себе) выполняются последовательно. Рассмотрим этот пример:
whichQueueShouldIUse.syncOrAsync {
for i in 1...10 {
print(i)
}
for i in 1...10 {
print(i + 100)
}
for i in 1...10 {
print(i + 1000)
}
}
Независимо от того, как вы настраиваете (последовательный или параллельный) или отправляете (синхронизирующий или асинхронный) эту очередь, эта задача всегда будет выполняться последовательно. Третий цикл никогда не будет выполняться перед вторым циклом, а второй цикл никогда не будет выполняться перед первым циклом. Это верно для любой очереди с любой диспетчеризацией. Это когда вы вводите несколько задач и / или очередей, где действительно вступают в игру последовательный интерфейс и параллелизм.
Рассмотрим эти две очереди, одну последовательную и одну параллельную:
let serialQueue = DispatchQueue(label: "serial")
let concurrentQueue = DispatchQueue.global(qos: .default)
Скажем, мы отправляем две параллельные очереди в асинхронном режиме:
concurrentQueue.async {
for i in 1...5 {
print(i)
}
}
concurrentQueue.async {
for i in 1...5 {
print(i + 100)
}
}
1
101
2
102
103
3
104
4
105
5
Их вывод беспорядочный (как и ожидалось), но обратите внимание, что каждая очередь последовательно выполняла свою задачу. Это самый простой пример параллелизма - две задачи, выполняемые одновременно в фоновом режиме в одной очереди. А теперь сделаем первый серийник:
serialQueue.async {
for i in 1...5 {
print(i)
}
}
concurrentQueue.async {
for i in 1...5 {
print(i + 100)
}
}
101
1
2
102
3
103
4
104
5
105
Разве первая очередь не должна выполняться последовательно? Было (и было второе). Что бы еще ни произошло в фоновом режиме, очередь не касается. Мы сказали последовательной очереди выполняться последовательно, и она сделала ... но мы дали ей только одну задачу. Теперь давайте дадим ему две задачи:
serialQueue.async {
for i in 1...5 {
print(i)
}
}
serialQueue.async {
for i in 1...5 {
print(i + 100)
}
}
1
2
3
4
5
101
102
103
104
105
И это самый простой (и единственно возможный) пример сериализации - две задачи, выполняющиеся последовательно (одна за другой) в фоновом режиме (для основного потока) в одной очереди. Но если мы сделаем их двумя отдельными последовательными очередями (потому что в приведенном выше примере это одна и та же очередь), их вывод снова будет беспорядочным:
serialQueue.async {
for i in 1...5 {
print(i)
}
}
serialQueue2.async {
for i in 1...5 {
print(i + 100)
}
}
1
101
2
102
3
103
4
104
5
105
Именно это я имел в виду, когда сказал, что все очереди параллельны друг относительно друга. Это две последовательные очереди, выполняющие свои задачи одновременно (потому что это отдельные очереди). Очередь не знает других очередей и не заботится о них. Теперь давайте вернемся к двум последовательным очередям (из той же очереди) и добавим третью очередь, параллельную:
serialQueue.async {
for i in 1...5 {
print(i)
}
}
serialQueue.async {
for i in 1...5 {
print(i + 100)
}
}
concurrentQueue.async {
for i in 1...5 {
print(i + 1000)
}
}
1
2
3
4
5
101
102
103
104
105
1001
1002
1003
1004
1005
Это своего рода неожиданность, почему параллельная очередь ожидала завершения последовательных очередей, прежде чем она была выполнена? Это не параллелизм. Ваша игровая площадка может показывать другой результат, но моя показала это. И он показал это, потому что приоритет моей параллельной очереди был недостаточно высок, чтобы GCD мог выполнить свою задачу раньше. Поэтому, если я сохраню все то же самое, но изменю QoS глобальной очереди (ее качество обслуживания, которое является просто уровнем приоритета очереди) let concurrentQueue = DispatchQueue.global(qos: .userInteractive)
, то результат будет таким, как ожидалось:
1
1001
1002
1003
2
1004
1005
3
4
5
101
102
103
104
105
Две последовательные очереди выполняли свои задачи последовательно (как и ожидалось), а параллельная очередь выполняла свою задачу быстрее, потому что ей был присвоен высокий уровень приоритета (высокий QoS или качество обслуживания).
Две параллельные очереди, как и в нашем первом примере печати, показывают беспорядочную распечатку (как и ожидалось). Чтобы заставить их печатать аккуратно в последовательном порядке, нам нужно было бы создать для них одну и ту же последовательную очередь (также один и тот же экземпляр этой очереди, а не одну и ту же этикетку) . Затем каждая задача выполняется последовательно по отношению к другой. Однако другой способ заставить их печатать по порядку - сохранить их одновременно, но изменить их метод отправки:
concurrentQueue.sync {
for i in 1...5 {
print(i)
}
}
concurrentQueue.async {
for i in 1...5 {
print(i + 100)
}
}
1
2
3
4
5
101
102
103
104
105
Помните, что диспетчеризация синхронизации означает только то, что вызывающий поток ожидает завершения задачи в очереди, прежде чем продолжить. Предостережение здесь, очевидно, заключается в том, что вызывающий поток замораживается до завершения первой задачи, что может или не может быть так, как вы хотите, чтобы пользовательский интерфейс выполнялся.
И именно по этой причине мы не можем делать следующее:
DispatchQueue.main.sync { ... }
Это единственная возможная комбинация очередей и методов диспетчеризации, которую мы не можем выполнить - синхронная диспетчеризация в основной очереди. И это потому, что мы просим основную очередь «заморозиться», пока мы не выполним задачу в фигурных скобках ... которую мы отправили в основную очередь, которую мы просто заморозили. Это называется тупиком. Чтобы увидеть это в действии на детской площадке:
DispatchQueue.main.sync { // stop the main queue and wait for the following to finish
print("hello world") // this will never execute on the main queue because we just stopped it
}
// deadlock
И последнее, о чем стоит упомянуть, - это ресурсы. Когда мы даем очереди задачу, GCD находит доступную очередь из своего пула, управляемого изнутри. Что касается написания этого ответа, для каждого qos доступно 64 очереди. Может показаться, что это много, но они могут быть быстро использованы, особенно сторонними библиотеками, особенно фреймворками баз данных. По этой причине у Apple есть рекомендации по управлению очередью (упомянутые в ссылках ниже); одно существо:
Вместо создания частных параллельных очередей отправляйте задачи в одну из глобальных параллельных очередей отправки. Для последовательных задач установите цель вашей последовательной очереди на одну из глобальных параллельных очередей.
Таким образом, вы можете поддерживать сериализованное поведение очереди, сводя к минимуму количество отдельных очередей, создающих потоки.
Для этого вместо того, чтобы создавать их, как мы делали раньше (что вы все еще можете), Apple рекомендует создавать такие последовательные очереди:
let serialQueue = DispatchQueue(label: "serialQueue", qos: .default, attributes: [], autoreleaseFrequency: .inherit, target: .global(qos: .default))
Для дальнейшего чтения рекомендую следующее:
https://developer.apple.com/library/archive/documentation/General/Conceptual/ConcurrencyProgrammingGuide/Introduction/Introduction.html#//apple_ref/doc/uid/TP40008091-CH1-SW1
https://developer.apple.com/documentation/dispatch/dispatchqueue