По-перше, важливо знати різницю між потоками та чергами та тим, що насправді робить 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")
Ви можете зробити його одночасно через властивість атрибута:
let concurrentQueue = DispatchQueue(label: "concurrent", attributes: [.concurrent])
Але на даний момент, якщо ви не додаєте жодних інших атрибутів до приватної черги, Apple рекомендує просто використовувати одну з їхніх готових до роботи глобальних черг (які є одночасно). У нижній частині цієї відповіді ви побачите інший спосіб створення послідовних черг (використовуючи цільову властивість), саме так Apple рекомендує це робити (для більш ефективного управління ресурсами). Але наразі маркування цього достатньо.
СУЧАСНІ ЧАСИ (часто відомі як глобальні черги відправлення) можуть виконувати завдання одночасно; завдання, однак, гарантовано починаються в тому порядку, коли вони були додані до цієї конкретної черги, але на відміну від послідовних черг, черга не чекає, коли перше завдання закінчиться перед початком другого завдання. Завдання (як і послідовні черги) виконуються на різних потоках і (як і в послідовних чергах), не кожне завдання гарантується для виконання на одному потоці (не важливо, але цікаво знати). Рамка 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)
Скажімо, ми відправляємо дві паралельні черги в системі async:
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
Дві послідовні черги виконували свої завдання послідовно (як і очікувалося), а паралельна черга виконувала своє завдання швидше, оскільки їй було надано високий рівень пріоритетності (високий рівень якості або якість обслуговування).
Дві паралельні черги, як у нашому першому прикладі друку, показують змішану роздруківку (як очікувалося). Щоб змусити їх друкувати акуратно послідовно, нам доведеться зробити їх обох однаковою послідовною чергою (той самий екземпляр цієї черги, а не лише ту саму мітку) . Потім кожне завдання виконується послідовно щодо іншого. Інший спосіб, однак, змусити їх друкувати серійно - це тримати їх одночасно, але змінити метод відправки:
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