paint-brush
Grand Central Dispatch, раз і назаўжды па@kfamyn
1,011 чытанні
1,011 чытанні

Grand Central Dispatch, раз і назаўжды

па Kiryl Famin20m2025/02/28
Read on Terminal Reader

Занадта доўга; Чытаць

У гэтым артыкуле разбіраецца Grand Central Dispatch (GCD) Swift, тлумачачы патокі, чэргі і блокі кода, а таксама сінхранізацыю супраць асінхроннага выканання, QoS і тупіковыя блакіроўкі — выкарыстоўваючы практычныя практыкаванні, каб замацаваць ваша разуменне.
featured image - Grand Central Dispatch, раз і назаўжды
Kiryl Famin HackerNoon profile picture
0-item
1-item

Прывітанне! Мяне завуць Кірыл Фамін, я iOS-распрацоўшчык.


У гэтым артыкуле мы раз і назаўсёды разгледзім Grand Central Dispatch (GCD) . Нягледзячы на тое, што GCD можа здацца састарэлым цяпер, калі існуе Swift Modern Concurrency, код, які выкарыстоўвае гэтую структуру, будзе працягваць з'яўляцца на працягу многіх гадоў - як у вытворчасці, так і ў інтэрв'ю.


Сёння мы засяродзімся выключна на фундаментальным разуменні GCD. Мы падрабязна разгледзім толькі ключавыя аспекты шматструменнасці, у тым ліку ўзаемасувязь паміж чэргамі і патокамі — тэма, якую многія іншыя артыкулы звычайна забываюць. З улікам гэтых паняццяў вам будзе прасцей зразумець такія тэмы, як DispatchGroup , DispatchBarrier , семафор, м'ютэкс і г.д.


Гэты артыкул будзе карысная як пачаткоўцам, так і вопытным распрацоўнікам. Я паспрабую растлумачыць усё зразумелай мовай, пазбягаючы лішку тэхнічных тэрмінаў.

Агляд зместу

  • Асноўныя паняцці: паток, шматструменнасць, GCD, задача, чарга
  • Віды чэргаў: асноўная, глабальная, заказная
  • Прыярытэты чаргі: якасць абслугоўвання (QoS)
  • Паслядоўныя і адначасовыя чэргі
  • Спосабы выканання задач: async, sync
  • Тупік
  • ГКД практыкаванні
  • Спасылкі

Асноўныя паняцці: паток, шматструменнасць, GCD, задача і чарга

Паток - па сутнасці, кантэйнер, дзе змяшчаецца і выконваецца набор сістэмных інструкцый. Фактычна ўвесь выканальны код працуе ў нейкім патоку. Мы адрозніваем асноўны паток і рабочыя патокі.

Патокі ў дадатку


Шматструменнасць - здольнасць сістэмы адначасова (адначасова) выконваць некалькі патокаў. Гэта дазваляе некалькім галінам кода працаваць паралельна.


Grand Central Dispatch (GCD) – структура, якая палягчае працу з патокамі (выкарыстоўваючы перавагі шматструменнасці). Яго асноўныя прымітывы - задачы і чэргі.


Такім чынам, GCD - гэта інструмент, які дазваляе лёгка пісаць код, які выконваецца адначасова. Просты прыклад - перанос цяжкіх вылічэнняў у асобны паток, каб не перашкаджаць абнаўленню карыстальніцкага інтэрфейсу ў галоўным патоку.


Задача - набор інструкцый, згрупаваных разам распрацоўшчыкам. Важна разумець, што распрацоўшчык вырашае, які код адносіцца да той ці іншай задачы.

Напрыклад:


 print(“GCD”) // a task


 let database = Database() let person = Person(age: 23) // also a task database.store(person)


Чарга - фундаментальны прымітыў GCD, гэта месца, куды распрацоўшчык ставіць задачы на выкананне. Чарга бярэ на сябе адказнасць за размеркаванне задач паміж патокамі (кожная чаргу мае доступ да пулу патокаў сістэмы).


Па сутнасці, чэргі дазваляюць засяродзіцца на арганізацыі кода ў задачах, а не на кіраванні патокамі непасрэдна. Калі вы адпраўляеце задачу ў чаргу, яна будзе выканана ў даступным патоку — часта адрозным ад таго, які выкарыстоўваецца для адпраўкі задачы.


Чарга


Вы можаце знайсці mp4-версіі ўсіх GIF-файлаў тут або ў раздзеле «Спасылкі» ніжэй.

Віды чэргаў

  1. Галоўная чарга - чарга, якая выконваецца толькі ў галоўным патоку. Ён серыйны (пра гэта пазней).

     let mainQueue = DispatchQueue.main


  2. Глабальныя чэргі – сістэмай прадугледжана 5 чэргаў (па адной для кожнага ўзроўню прыярытэту). Яны адначасовыя.

     let globalQueue = DispatchQueue.global()


  3. Карыстальніцкія чэргі – чэргі, створаныя распрацоўшчыкам. Распрацоўшчык выбірае адзін з 5 прыярытэтаў і тып: паслядоўны або паралельны (па змаўчанні яны паслядоўныя).

     let userQueue = DispatchQueue(label: “com.kirylfamin.concurrent”, attributes: .concurrent).

Прыярытэты чаргі

Якасць абслугоўвання (QoS) – сістэма прыярытэтаў у чарзе. Чым вышэй прыярытэт чаргі, у якую стаіць задача, тым больш рэсурсаў ёй выдзяляецца. Усяго існуе 5 узроўняў QoS:


  1. .userInteractive – самы высокі прыярытэт. Ён выкарыстоўваецца для задач, якія патрабуюць неадкладнага выканання, але не падыходзяць для выканання ў галоўным патоку. Напрыклад, у дадатку, які дазваляе рэтушаваць выявы ў рэальным часе, вынік рэтушы павінен быць вылічаны імгненна; аднак, калі гэта зрабіць у галоўным патоку, гэта будзе перашкаджаць абнаўленню карыстацкага інтэрфейсу і апрацоўцы жэстаў, якія заўсёды адбываюцца ў галоўным патоку (напрыклад, калі карыстальнік праводзіць пальцам па вобласці, якую трэба рэтушаваць, і праграма павінна імгненна адлюстроўваць вынік «пад пальцам»). Такім чынам, мы атрымліваем вынік максімальна хутка, не абцяжарваючы асноўную нітку.


  2. .userInitiated – прыярытэт для задач, якія патрабуюць хуткай зваротнай сувязі, хоць і не так крытычна, як інтэрактыўныя задачы. Звычайна ён выкарыстоўваецца для задач, у якіх карыстальнік разумее, што задача не будзе выканана імгненна і ёй прыйдзецца пачакаць (напрыклад, запыт сервера).


  3. .default – стандартны прыярытэт. Ён прызначаецца, калі распрацоўшчык не вызначае QoS пры стварэнні чаргі - калі няма асаблівых патрабаванняў да задачы і яе прыярытэт не можа быць вызначаны з кантэксту (напрыклад, калі вы выклікаеце задачу з чаргі з прыярытэтам .userInitiated, задача ўспадкоўвае гэты прыярытэт).


  4. .utility – прыярытэт для задач, якія не патрабуюць неадкладнай зваротнай сувязі з карыстальнікам, але неабходныя для працы праграмы. Напрыклад, сінхранізацыя дадзеных з серверам або запіс аўтазахавання на дыск.


  5. .background – самы нізкі прыярытэт. Прыклад - ачыстка кэша.


Усе чэргі класіфікуюцца як паслядоўныя або адначасовыя чэргі


  • Паслядоўныя чэргі - як вынікае з назвы, гэта чэргі, у якіх задачы выконваюцца адна за адной. Гэта азначае, што наступнае заданне пачынаецца толькі пасля завяршэння бягучага .


  • Паралельныя чэргі - гэтыя чэргі дазваляюць задачам выконваць паралельна - новая задача пачынаецца, як толькі рэсурсы размеркаваны, незалежна ад таго, ці былі выкананы папярэднія задачы. Звярніце ўвагу, што гарантуецца толькі парадак пачатку (заданне, пастаўленае ў чаргу раней, пачнецца раней за наступнае), але парадак завяршэння не гарантуецца.

    Паслядоўныя і адначасовыя чэргі


Як выконваць задачы

Важна адзначыць, што зараз мы абмяркоўваем метады выканання задач адносна выклікаючага патоку . Іншымі словамі, спосаб, якім вы выклікаеце задачу, вызначае, як разгортваюцца падзеі ў патоку, з якога вы адпраўляеце задачу ў чаргу.

Асінхронна ( async )

Асінхронны выклік - гэта той, у якім выклікаючы паток не блакуецца - гэта значыць, ён не чакае выканання задачы, пастаўленай у чаргу.

 DispatchQueue.main.async { print(“A”) } print(“B”)


У гэтым прыкладзе мы асінхронна ставім у чаргу задачу print("A") у асноўнай чарзе з асноўнага патоку (паколькі гэты код не знаходзіцца ў якой-небудзь канкрэтнай чарзе, ён па змаўчанні выконваецца ў галоўным патоку). Такім чынам, мы не чакаем заданне ў галоўным патоку і неадкладна працягваем выкананне. У гэтым канкрэтным прыкладзе задача print("A") ставіцца ў галоўную чаргу , а затым print("B") неадкладна выконваецца ў галоўным патоку . Паколькі галоўны паток заняты выкананнем бягучага кода (а задачы з асноўнай чаргі могуць выконвацца толькі ў галоўным патоку), бягучая задача print("B") завяршаецца першай, і толькі пасля таго, як асноўны паток вызваліцца, запускаецца задача print("A") пастаўленая ў чаргу ў асноўнай чарзе . Выхад: BA.


 DispatchQueue.global().async { updateData() DispatchQueue.main.async { updateInterface() } Logger.log(.success) } indicateLoading()


  1. Мы асінхронна дадаем задачу ў глабальную чаргу з прыярытэтам па змаўчанні з асноўнага патоку — таму паток, які выклікае, неадкладна працягваецца і выклікае indicateLoading() .


  2. Праз некаторы час сістэма выдзяляе рэсурсы для задачы і выконвае яе ў вольным рабочым патоку з пула патокаў, і выклікаецца updateData() .


  3. Задача, якая змяшчае updateInterface() , асінхронна ставіцца ў асноўную чаргу — рабочы паток, які выклікае выклік, не чакае яго завяршэння і працягвае працу.


  4. Паколькі задачы ставяцца ў чаргу асінхронна, мы не можам быць упэўнены, калі рэсурсы будуць размеркаваны. У гэтым выпадку мы не можам з упэўненасцю сказаць, ці будзе updateInterface() (у галоўным патоку) або Logger.log(.success) (у працоўным патоку) выкананы першым (мы таксама не можам на кроках 1-2: тое, што выконваецца першым, indicateLoading() у галоўным патоку або updateData() у працоўным патоку). Хоць асноўны паток заняты апрацоўкай абнаўленняў карыстальніцкага інтэрфейсу, апрацоўкай жэстаў і іншымі базавымі задачамі, тым не менш ён заўсёды атрымлівае максімум сістэмных рэсурсаў. З іншага боку, рэсурсы для выканання ў працоўным патоку таксама могуць быць выдзелены амаль адразу.



Асінхронны выклік


Звярніце ўвагу, што ў гэтай анімацыі глабальная чарга выконвае свае задачы ў некаторым вольным рабочым патоку

Сінхронна ( sync )

Сінхронны выклік - гэта той, калі выклікаючы паток спыняецца і чакае завяршэння задачы, якую ён паставіў у чаргу.

 let userQueue = DispatchQueue(label: "com.kirylfamin.serial") DispatchQueue.global().async { var account = BankAccount() userQueue.sync { account.balance += 10 } let balance = account.balance print(balance) }


Тут з працоўнага патоку, які выконвае задачу ў глабальнай чарзе, мы сінхронна ставім задачу ў карыстальніцкую чаргу, каб павялічыць баланс. Бягучы паток заблакіраваны і чакае завяршэння задачы ў чарзе. Такім чынам, баланс друкуецца толькі пасля завяршэння прырашчэння задання ў карыстальніцкай чарзе.



Сінхронная задача


Заўвага: у прыведзенай вышэй анімацыі карыстальніцкая чарга выконвае свае задачы ў некаторым бясплатным рабочым патоку

Тупік

У кантэксце сінхронных задач важна абмеркаваць тупік - калі паток або патокі бясконца чакаюць, пакуль самі або адзін для аднаго працягнуцца. Самы распаўсюджаны прыклад - выклік DispatchQueue.main.sync {} з галоўнага патоку .


Асноўны паток заняты выкананнем бягучай задачы, у рамках якой мы хочам сінхронна выканаць нейкі код. Такім чынам, сінхронны выклік блакуе асноўны паток . Задача пастаўлена ў галоўную чаргу , але не можа быць запушчана, таму што галоўны паток заблакіраваны ў чаканні завяршэння бягучага задання, а заданні ў галоўнай чарзе могуць выконвацца толькі ў галоўным патоку . Спачатку гэта можа быць цяжка ўявіць, але галоўнае - разумець, што задача, пастаўленая ў чаргу з дапамогай DispatchQueue.main.sync становіцца часткай бягучай задачы, і мы ставім яе ў чаргу пасля бягучай задачы. У выніку паток чакае часткі бягучай задачы, якая не можа быць запушчана, таму што паток заняты бягучай задачай.


 func printing() { print(“A”) DispatchQueue.main.sync { print(“B”) } print(“C”) } 

Тупік


Звярніце ўвагу, што print("B") з асноўнай чаргі не можа быць выканана, таму што асноўны паток заблакаваны.

GCD Практыкаванні

У гэтым раздзеле з усімі набытымі да гэтага часу ведамі мы абмяркуем практыкаванні рознай складанасці: ад простых блокаў кода, з якімі вы сутыкнецеся падчас інтэрв'ю, да складаных задач, якія падштурхнуць ваша разуменне паралельнага праграмавання. Пытанне ва ўсіх гэтых задачах: што будзе надрукавана на кансолі?


Памятайце, што асноўная чарга з'яўляецца паслядоўнай, чэргі global() з'яўляюцца адначасовымі, і часам праблема можа ўключаць карыстальніцкія чэргі з пэўнымі атрыбутамі.

Базавыя практыкаванні

Мы пачнем з задач звычайнай складанасці - тых, якія маюць невялікую верагоднасць нявызначанасці ў вывадзе. Гэтыя заданні часцей за ўсё з'яўляюцца на інтэрв'ю; галоўнае - не спяшацца і старанна прааналізаваць праблему.

Вы можаце знайсці поўны код усіх практыкаванняў тут .


Заданне 1

 print(“A”) DispatchQueue.main.async { print(“B”) } print(“C”)


  1. У галоўным патоку выконваецца print("A") .
  2. Задача для print("B") асінхронна ставіцца ў асноўную чаргу. Паколькі асноўны паток заняты, гэтая задача чакае ў чарзе.
  3. У галоўным патоку выконваецца print("C") .
  4. Калі асноўны паток вольны (пасля выканання папярэдняга задання могуць узнікнуць іншыя падзеі, якія патрабуюць апрацоўкі ў галоўным патоку — не толькі задачы з асноўнай чаргі, такія як абнаўленні карыстальніцкага інтэрфейсу, апрацоўка жэстаў і г.д. Для больш глыбокага разумення, калі ласка, прачытайце больш пра RunLoop ), у чарзе выконваецца задача print("B") .


Адказ : ACB


Заданне 2

 print(“A”) DispatchQueue.main.async { print(“B”) } DispatchQueue.main.async { print(“C”) } print(“D”)


  1. У галоўным патоку выконваецца print("A") .
  2. Задача для print("B") ставіцца ў галоўную чаргу. Галоўная чарга, пакуль асноўны паток не стане даступным.
  3. Задача для print("C") ставіцца ў чаргу пасля print("B") і таксама чакае.
  4. Асноўны паток працягвае выкананне і друкуе "D".
  5. Калі асноўны паток становіцца даступным (пасля апрацоўкі іншых задач RunLoop), выконваецца першая аперацыя ў чарзе print("B") .
  6. Пасля таго, як асноўны паток зноў становіцца свабодным (пасля апрацоўкі іншых задач RunLoop — у будучыні я буду апускаць гэтую дэталь, бо яна не ўплывае на агульны парадак), выконваецца задача для print("C") .


Адказ : ADBC


Адразу зазначу, што ў некаторых прыкладах я крыху спрасчу тлумачэнне і апущу той факт, што сістэма аптымізуе выкананне сінхронных выклікаў, пра што мы пагаворым пазней.


Заданне 3

 print(“A”) DispatchQueue.main.async { // 1 print(“B”) DispatchQueue.main.async { print(“C”) } DispatchQueue.global().sync { print(“D”) } DispatchQueue.main.sync { // 2 print(“E”) } } // 3 print(“F”) DispatchQueue.main.async { print(“G”) }


  1. print("A") выконваецца ў галоўным патоку.
  2. Асінхронная задача (пазначаная 1–3) ставіцца ў галоўную чаргу без блакіроўкі бягучага (асноўнага) патоку.
  3. Галоўны паток працягвае выкананне і друкуе "F" .
  4. Аперацыя print("G") ставіцца ў галоўную чаргу пасля папярэдняй задачы (крокі 1–3).
  5. Як толькі асноўны паток вызваляецца, першая аперацыя ў чарзе print("B") —пачынае выкананне.
  6. Затым аперацыя print("C") ставіцца ў галоўную чаргу (дзе бягучая задача ўсё яшчэ выконваецца, а print("G") ідзе за ёй у чарзе). Паколькі ён дадаецца асінхронна, мы не чакаем яго выканання і адразу рухаемся далей.
  7. Затым аперацыя print("D") ставіцца ў глабальную чаргу. Паколькі гэты выклік сінхронны, мы чакаем, пакуль глабальная чарга выканае яго (яна можа працаваць у любым даступным працоўным патоку), перш чым працягнуць.
  8. Нарэшце, аперацыя print("E") ставіцца ў асноўную чаргу. Паколькі гэты выклік сінхронны, бягучы паток павінен быць заблакіраваны да завяршэння задачы. Аднак у асноўнай чарзе ўжо ёсць задачы, і аперацыя print("E") дадаецца ў канец, пасля іх. Такім чынам, перад запускам print("E") павінны быць выкананы гэтыя аперацыі. Але асноўны паток усё яшчэ заняты выкананнем бягучай аперацыі, таму ён не можа перайсці да наступных аперацый у чарзе. Нават калі не было ніякіх аперацый для друку "G" і "C" пасля бягучай аперацыі, паток усё роўна не можа працягвацца, таму што бягучая аперацыя (крокі 1–3) яшчэ не завершана.
  • Калі б выклік быў асінхронным, аперацыя print("E") проста была б пастаўлена ў чаргу пасля аперацый друку "G" і "C" .


Адказ : AFBD

Альтэрнатыўны адказ (калі другі выклік быў async ): AFBDGCE


Заданне 4

 let serialQueue = DispatchQueue(label: “com.kirylfamin.serial”) serialQueue.async { // 1 print(“A”) serialQueue.sync { print(“B”) } print(“C”) } // 2

  1. Задача (крокі 1–2) асінхронна ставіцца ў карыстальніцкую паслядоўную чаргу (па змаўчанні чэргі паслядоўныя, бо мы не выкарыстоўвалі атрыбут .concurrent ).

  2. Калі сістэма размяркоўвае рэсурсы, пачынаецца выкананне і друкуецца "A" .
  3. У той жа паслядоўнай чарзе сінхронная задача для print("B") ставіцца ў чаргу. Паколькі выклік сінхронны, паток блакіруецца ў чаканні яго выканання.
  4. Аднак, паколькі чарга паслядоўная і ўсё яшчэ занятая знешняй задачай 1-2, задача print("B") не можа запусціцца, што прыводзіць да тупіка.


Адказ : A, тупік

Гэты прыклад паказвае, што тупік можа адбыцца ў любой паслядоўнай чарзе — у асноўнай або карыстальніцкай.


Заданне 5

Давайце заменім паслядоўную чаргу з папярэдняй задачы на адначасовую.

 DispatchQueue.global().async { // 1 print("A") DispatchQueue.global().sync { print("B") } print("C") } // 2


  1. Задача (крокі 1–2) асінхронна ставіцца ў глабальную (паралельную) чаргу.
  2. Калі рэсурсы размеркаваны, пачынаецца выкананне і друкуецца "A" .
  3. Здзяйсняецца сінхронны выклік для выканання print("B") у той жа глабальнай чарзе, які блакіруе бягучы рабочы паток да завяршэння задачы.
  4. У гэтым выпадку, нават калі паток заблакіраваны, паколькі глабальная чарга адначасовая, яна можа пачаць выкананне наступнай аперацыі, не чакаючы завяршэння бягучай — проста запусціўшы яе ў іншым патоку. Такім чынам, выклікаючы паток чакае выканання задачы print("B") у іншым рабочым патоку.
  5. Пасля выканання задання першапачатковы паток выкліку разблакуецца, друкуецца "C" .

Адказ : ABC


Заданне 6

 print("A") DispatchQueue.main.async { // 1 print("B") DispatchQueue.main.async { // 2 print("C") DispatchQueue.main.async { // 3 print("D") DispatchQueue.main.sync { print("E") } } // 4 } // 5 DispatchQueue.global().sync { // 6 print("F") DispatchQueue.global().sync { print("G") } } // 7 print("H") } // 8 print("I")


  1. Асноўная нітка друкуе "A" .

  2. Асінхронная задача (крокі 1–8) ставіцца ў галоўную чаргу без блакіроўкі бягучага патоку.

  3. Асноўная нітка працягваецца і друкуе "I" .

  4. Пазней, калі асноўны паток вольны, задача, пастаўленая ў асноўную чаргу, пачынае выкананне і друкуе "B" .

  5. Іншая асінхронная задача (крокі 2–5) ставіцца ў асноўную чаргу - не блакіруе бягучы паток.

  6. Працягваючы выкананне ў бягучым патоку, сінхронная адпраўка аперацыі 6–7 робіцца ў глабальную чаргу — гэта блакуе бягучы (асноўны) паток да завяршэння задачы.

  7. Аперацыі 6–7 пачынаюць выконвацца ў іншым патоку, друкуючы "F" .

  8. Аперацыя print("G") сінхронна адпраўляецца ў глабальную чаргу, блакіруючы бягучы працоўны паток да яго завяршэння.

  9. Друкуецца "G" , а рабочы паток, з якога была адпраўлена гэтая аперацыя, разблакуецца.

  10. Аперацыя 6–7 завяршаецца, разблакуючы паток, з якога ён быў адпраўлены (асноўны паток), і друкуецца "H" .

  11. Пасля завяршэння аперацыі 1–2 выкананне пераходзіць да наступнай аперацыі ў галоўнай чарзе — аперацыі 2–5 — якая пачынаецца і друкуе "C" .

  12. Аперацыя 3–4 ставіцца ў асноўную чаргу без блакіроўкі патоку.

  13. Пасля завяршэння бягучай аперацыі (2–5) пачынаецца выкананне наступнай аперацыі (3–4), друкуючы "D" .

  14. Аперацыя print("G") сінхронна адпраўляецца ў галоўную чаргу, блакуючы бягучы паток.

  15. Затым сістэма бясконца доўга чакае выканання аперацыі print("E") у галоўным патоку - паколькі паток заблакаваны, гэта прыводзіць да тупіка.


Адказ : AIBFGHCD, тупік

Прамежкавыя практыкаванні

Заданні сярэдняй цяжкасці звязаны з нявызначанасцю. Такія праблемы таксама сустракаюцца на сумоўях, хоць і рэдка.


Заданне 7

 DispatchQueue.global().async { print("A") } DispatchQueue.global().async { print("B") }


  1. print("A") асінхронна ставіцца ў глабальную чаргу — без блакавання бягучага патоку.
  2. Чакаем, пакуль сістэма вылучыць рэсурсы для задачы ў глабальнай чарзе. Тэарэтычна гэта можа адбыцца ў любы момант — нават да выканання наступнай каманды для пастаноўкі ў чаргу print("B") . У гэтым канкрэтным выпадку ў чаргу спачатку дадаецца наступная задача, і толькі потым рэсурсы размяркоўваюцца ў глабальную чаргу. Гэта адбываецца таму, што асноўнаму патоку выдзяляецца больш за ўсё рэсурсаў, а наступная аперацыя ў галоўным патоку вельмі лёгкая (толькі аперацыя дадання задачы), і на практыцы яна адбываецца хутчэй, чым размеркаванне рэсурсаў у глабальнай чарзе. Мы абмяркуем супрацьлеглыя сцэнары ў наступным раздзеле.
  3. print("B") стаіць у глабальнай чарзе.
  4. Тым часам асноўны паток працягваецца, пакуль глабальная чарга чакае размеркавання рэсурсаў.
  5. Калі рэсурсы становяцца даступнымі, абедзве задачы выконваюцца. Нягледзячы на тое, што друк задачы "A" можа пачацца раней, чым "B" , мы не можам гарантаваць парадак, таму што друк не з'яўляецца атамарнай аперацыяй (вывад з'яўляецца ў кансолі блізкім да канца аперацыі).


Адказ : (AB)

Дужкі паказваюць, што літары могуць стаяць у любым парадку: AB або BA.


Заданне 8

 print("A") DispatchQueue.main.async { print("B") } DispatchQueue.global().async { print("C") }

Тут мы можам быць упэўненыя толькі ў тым, што "А" надрукавана першай. Мы не можам дакладна вызначыць, ці будзе задача ў галоўнай чарзе або ў глабальнай чарзе выканана хутчэй.


Адказ : A(BC)


Заданне 9

 DispatchQueue.global(qos: .userInteractive).async { print(“A”) } DispatchQueue.main.async { // 1 print(“B”) }


і

 DispatchQueue.global(qos: .userInteractive).async { print(“A”) } print(“B”) // 1


З аднаго боку, у абодвух выпадках print("B") выконваецца ў галоўным патоку. Акрамя таго, мы не можам дакладна вызначыць, калі ў глабальнай чарзе будуць размеркаваны рэсурсы, таму тэарэтычна "A" можа быць надрукавана непасрэдна перад дасягненнем кропкі, пазначанай // 1 у галоўным патоку. На практыцы, аднак, першая задача заўсёды друкуецца як AB, а другая - як BA. Гэта таму, што ў першым выпадку print("B") выконваецца прынамсі ў наступнай ітэрацыі RunLoop асноўнага патоку (або некалькі ітэрацый пазней), тады як у другім выпадку print("B") запланаваны для выканання ў бягучай ітэрацыі RunLoop у галоўным патоку. Аднак мы не можам гарантаваць заказ.


Адказ для абодвух заданняў: (АВ)


Заданне 10

 print("A") DispatchQueue.global().async { print("B") DispatchQueue.global().async { print("C") } print("D") }

Зразумела, што пачатак вываду - "AB" . Пасля пастаноўкі ў чаргу print("C") мы не можам дакладна вызначыць, калі для гэтага будуць выдзелены рэсурсы — гэта задача можа быць выканана да або пасля print("D") . Гэта часам здараецца і на практыцы.


Адказ : AB(CD)


Заданне 11

 let serialQueue = DispatchQueue(label: “com.kirylfamin.serial”, qos: .userInteractive) DispatchQueue.main.async { print(“A”) serialQueue.async { print(“B”) } print(“C”) }

Зноў жа, мы не можам дакладна вызначыць, калі рэсурсы будуць выдзелены для друку ("B") у карыстальніцкай чарзе. На практыцы, паколькі галоўны паток мае самы высокі прыярытэт, "C" звычайна друкуецца перад "B", хоць гэта не гарантуецца.


Адказ : A(BC)


Заданне 12

 DispatchQueue.global().async { print("A") } print("B") sleep(1) print("C")

Тут відавочна, што вывад будзе BAC, таму што аднасекундны сон гарантуе, што ў глабальнай чарзе будзе дастаткова часу для размеркавання рэсурсаў. У той час як асноўны паток заблакаваны рэжымам сну (чаго не варта рабіць у вытворчасці), print("A") выконваецца ў іншым патоку.


Адказ : BAC


Заданне 13

 DispatchQueue.main.async { print("A") } print("B") sleep(1) print("C")

У гэтым выпадку, паколькі print("A") стаіць у галоўнай чарзе, ён можа быць выкананы толькі ў галоўным патоку. Тым не менш, асноўны паток працягвае выконваць код — друкуе "B" , затым спіць, потым друкуе "C" . Толькі пасля гэтага RunLoop можа выканаць задачу, пастаўленую ў чаргу.


Адказ : BCA

Павышаныя задачы

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

Клас Counter тут выкарыстоўваецца выключна для даведачнай семантыкі:

 final class Counter { var count = 0 }


Заданне 14

 let counter = Counter() DispatchQueue.global().async { DispatchQueue.main.async { print(counter.count) } for _ in (0..<100) { // 1 counter.count += 1 } }

Тут можа быць надрукавана любая лічба ад 0 да 100, у залежнасці ад таго, наколькі заняты асноўны паток. Як мы ведаем, мы не можам дакладна прадказаць, калі асінхронная задача атрымае рэсурсы - гэта можа адбыцца да, падчас або пасля цыкла ў працоўным патоку.


Адказ : 0-100


Заданне 15

 DispatchQueue.global(qos: .userInitiated).async { print(“A”) } DispatchQueue.global(qos: .userInteractive).async { print(“B”) }

QoS не гарантуе, што чарга з больш высокім прыярытэтам атрымае рэсурсы хутчэй, хоць iOS паспрабуе зрабіць гэта. На практыцы выхад тут (AB).


Адказ : (AB)


Заданне 16

 var count = 0 DispatchQueue.global(qos: . userInitiated).async { for _ in 0..<1000 { count += 1 } print(“A”) } DispatchQueue.global(qos: .userInteractive).async { for _ in 0..<1000 { count += 1 } print(“B”) }

Паколькі мы не можам ведаць, якое выкананне пачынаецца першым, нават на працягу 1000 аперацый мы не можам вызначыць, якая задача будзе выканана хутчэй.


Адказ : (AB)


Заданне 16.2

Які будзе вынік пры ўмове, што аперацыі пачынаюць выконвацца адначасова?


Паколькі чарзе .userInteractive выдзяляецца больш рэсурсаў, на працягу 1000 аперацый выкананне ў гэтай чарзе заўсёды будзе завяршацца хутчэй.


Адказ : BA


Заданне 17

Выкарыстоўваючы падобны падыход, мы можам змяніць любую задачу з нявызначанасцю з папярэдняга раздзела (напрыклад, задачу 12):


 let counter = Counter() let serialQueue = DispatchQueue(label: “com.kirylfamin.serial”, qos: .userInteractive) DispatchQueue.main.async { serialQueue.async { print(counter.count) } for _ in 0..<100 { counter.count += 1 } }


Можа быць надрукавана любая лічба ад 0 да 100. Той факт, што 0 можа быць надрукаваны, пацвярджае, што ў задачы 12 мы не можам гарантаваць, што вывад "C" заўсёды будзе адбывацца перад "B" , паколькі па сутнасці нічога не змянілася - толькі цыкл патрабуе крыху больш рэсурсаў, чым друк (звярніце ўвагу, што просты запуск цыкла, нават да яго выканання, на практыцы прывёў да поўнай нявызначанасці).

Адказ : 0-100


Заданне 18

 DispatchQueue.global(qos: .userInitiated).async { print(“A”) } print(“B”) DispatchQueue.global(qos: .userInteractive).async { print(“C”) }


Падобная сітуацыя адбываецца і тут. Тэарэтычна, print("A") можа выконвацца хутчэй, чым print("B") (калі вы заменіце print("B") чымсьці крыху больш цяжкім). На практыцы "B" заўсёды друкуецца першым. Аднак той факт, што мы выконваем print("B") перад тым, як паставіць у чаргу print("C") значна павялічвае верагоднасць таго, што "A" будзе надрукавана перад "C" , паколькі дадатковага часу, затрачанага на print("B") у галоўным патоку, часта бывае дастаткова для таго, каб чарга .userInitiated атрымала рэсурсы і выканала print("A") . Тым не менш, гэта не гарантавана, і часам "C" можа друкавацца хутчэй. Такім чынам, у тэорыі існуе поўная нявызначанасць; на практыцы, як правіла, B(CA).


Адказ : (BCA)


Заданне 19

 DispatchQueue.global().sync { print(Thread.current) }


У дакументацыі для сінхранізацыі гаворыцца:


«У якасці аптымізацыі прадукцыйнасці гэтая функцыя выконвае блокі ў бягучым патоку кожны раз, калі гэта магчыма, за адным выключэннем: блокі, адпраўленыя ў галоўную чаргу адпраўкі, заўсёды запускаюцца ў галоўным патоку».

Гэта азначае, што ў мэтах аптымізацыі сінхронныя выклікі могуць выконвацца ў тым жа патоку, з якога яны былі выкліканы (за выключэннем main.sync - задачы, якія выкарыстоўваюць яго, заўсёды выконваюцца ў галоўным патоку). Такім чынам, друкуецца бягучы (асноўны) паток.


Адказ : асноўная нітка


Заданне 20

 DispatchQueue.global().sync { // 1 print(“A”) DispatchQueue.main.sync { print(“B”) } print(“C”) }

Друкуецца толькі "A" таму што ўзнікае тупік. З-за аптымізацыі задача (пазначаная 1) пачынае выконвацца ў галоўным патоку, а затым выклік main.sync прыводзіць да тупіка.


Адказ : А, тупік


Заданне 21

 DispatchQueue.main.async { print("A") DispatchQueue.global().sync { print("B") } print("C") }


Аптымізацыя прыводзіць да таго, што задача print("B") не ставіцца ў чаргу, а "ўключаецца" ў бягучы паток выканання. Такім чынам, код:

 DispatchQueue.global().sync { print("B") }


становіцца эквівалентным:

 print(“B”)


Адказ : ABC


З гэтых задач становіцца ясна, што вы павінны выкарыстоўваць main.sync вельмі асцярожна - толькі калі вы ўпэўнены, што выклік зроблены не з галоўнага патоку.

Заключэнне

У гэтым артыкуле мы засяродзіліся на асноватворных канцэпцыях шматструменнасці ў iOS — патоках, задачах і чэргах — і іх узаемасувязях. Мы даследавалі, як GCD кіруе выкананнем задач у асноўнай, глабальнай і карыстальніцкай чэргах, і абмеркавалі адрозненні паміж паслядоўным і адначасовым выкананнем. Акрамя таго, мы вывучылі важныя адрозненні паміж сінхроннай (sync) і асінхроннай (async) адпраўкай задач, падкрэсліўшы, як гэтыя падыходы ўплываюць на парадак і час выканання кода. Авалоданне гэтымі базавымі паняццямі мае важнае значэнне для стварэння адаптыўных, стабільных прыкладанняў і для таго, каб пазбегнуць распаўсюджаных падводных камянёў, такіх як тупіковыя блакіроўкі.


Спадзяюся, вы знайшлі што-небудзь карыснае ў гэтым артыкуле. Калі што-небудзь застаецца незразумелым, не саромейцеся звяртацца да мяне за бясплатным тлумачэннем у Telegram: @kfamyn .

Адпаведныя спасылкі

  1. YouTube канал з усімі анімацыямі - https://www.youtube.com/@kirylfamin
  2. Поўны код практыкаванняў - https://github.com/kfamyn/GCD-Tasks
  3. Мой Telegram - http://t.me/kfamyn
  4. RunLoop - https://developer.apple.com/documentation/foundation/runloop
  5. дакументацыя метаду sync - https://developer.apple.com/documentation/dispatch/dispatchqueue/sync(execute:)-3segw