متین عبداللهی
متین عبداللهی
خواندن ۹ دقیقه·۵ سال پیش

GCD, Multi-Threading, DispatchQueue,DispatchGroup


Grand Central Dispatch
Grand Central Dispatch

توی این مقاله قصد دارم تا درباره multi-threading در Swift بنویسم و شما رو با GCD و کلاس های مهمی مثل DispatchQueue و DispatchGroup آشنا کنم.


قبل از همه چی

قبل از اینکه بخواییم درباره GCD حرف بزنیم باید یه درک قابل قبولی از threading و concurrency در برنامه نویسی داشته باشیم. پس اگر فکر می‌کنید که در این‌ باره به اندازه کافی میدونید، میتونید این مرحله رو رد کنید.

بریم سراغ Threading & Concurrency

توی iOS هر برنامه ای که اجرا می‌کنید از یک یا چند thread تشکیل شده. اینکه thread ها چطوری و چه زمان باید اجرا بشن، توی پایین ترین لایه، کاملا به عهده سیستم عامل دستگاه می‌باشد.

دیوایس هایی که تنها یک هسته دارن (single-core devices)، برای انجامِ دو یا چند کار به صورت هم زمان از روشی به اسم time-slicing استفاده می‌کنن.

حالا این روش چطور کار میکنه؟! فرض کنید دوتا thread داریم که می‌خواییم به صورت هم زمان، هر کدومشون یه کاری رو برای ما انجام بدن. توی این روش پردازنده خیلی سریع بین thread ها switch می‌کنه و مقداری از کار هر thread رو انجام میده. به همین ترتیب کار هر دو thread رو همزمان باهم پیش میبره. تصویر زیر نشون میده این کار چطور انجام میشه:

single-core devices
single-core devices

از طرف دیگه دیوایس های چند هسته ای (multi-core devices) رو داریم که میتونن thread ها رو به صورت هم زمان و موازی باهم (parallelism) به اجرا در بیارن.

multi-core devices
multi-core devices

اصل مطلب - GCD و DispatchQueue

سه حرف GCD مخفف Grand Central Dispatch هستن. GCD یک کلاس یا یک متد نیست! بلکه یک Low Level API برای مدیریت task ها بصورت همزمان است تا از freez شدن اپلیکیشن جلوگیری کنه. تعریف اپل از GCD:

"GCD provides and manages FIFO queues to which your application can submit tasks in the form of block objects. Work submitted to dispatch queues are executed on a pool of threads fully managed by the system. No guarantee is made as to the thread on which a task executes."

این سه کلمه ترجمه تحت الفظیشون به زبان فارسی معنای واضح و قابل درکی نداره. اما شاید اگر بدونید چرا اپل این اسم رو انتخاب کرده، راحت تر بتونید درکش کنید.

در واقع GCD اشاره میکنه به یک پایانه راه آهن مسافربری در شهر نیویورک، که به اسم Grand Central Terminal شناخته میشه. GCD به کمک کلاس قدرتمند DispatchQueue - که در ادامه بیشتر باهاش آشنا میشیم - عملکرد مشابه به یکی از کارهای اصلی پایانه مسافربری، یعنی اعزام قطارها داره.

بیایین فرض کنیم هر یک از task ها نقش قطار و هر یک از queueها (صف ها) نقش ریل ها رو بازی می‌کنن. با استفاده از کلاس DispatchQueue تعیین میکنیم هر قطار چه زمانی و از کدوم ریل اعزام بشه. بعضی از ریل ها میتونن چند قطار رو همزمان باهم از خودشون عبور بدن و بعضی از ریل ها فقط یک قطار رو در هر لحظه از خودشون عبور میدن و شرط عبور قطار بعدی از این نوع ریل ها اینه که قطار قبلی به مقصد رسیده باشه. ریلی که همزمان چند قطار رو از خودش عبور میده در واقع شبیه به صفی است که میتونه همزمان چند task رو اجرا کنه. و ریلی که فقط یک قطار رو در هر لحظه از خودش عبور میده درواقع مانند صفی است که در هر لحظه فقط یک task رو میتونه اجرا کنه و شرط اجرای task بعدی در این صف، به پایان رسیدن task قبلیست.

بنابرین کلاس DispatchQueue که به فارسی به معنی صف اعزام است، میتونه صف تشکیل بده و task هایی رو به دو طریق sync یا async بهش اعزام کنه. همچنین می‌تونیم بجای استفاده از closure در متد های sync و async، آبجکتی از کلاس DispatchWorkItem به این دو متد پاس بدیم.

DispatchQueue
DispatchQueue


انواع DiaptchQueue

کلاس DispatchQueue یکی از کلاس های تعریف شده در GCD است که با استفاده از اون میشه به سادگی task هایی رو به صورت همزمان انجام داد. این کلاس صف هایی مستقل از هم تشکیل میده که هر صف وظیفه اجرای task هایی که بهش اعزام میشن رو بر عهده داره. حالا اینکه task های اعزام شده به صف، به صورت متوالی (serial) یا هم‌زمان (concurrent) اجرا بشن، به نوع صفی که تعریف کردیم بستگی داره.

انواع صف ها:

  • نوع۱ - Concurrent Queue: در این نوع صف یک یا چند task به صورت همزمان اجرا می‌شن. ترتیب اجرای task ها بر اساس قاعده FIFO می‌باشد. به این معنی که هر task که زود تر اعزام شده باشه، زود تر اجرا می‌شه. اینکه در هر صف دقیقا چه تعداد task می‌تونه اجرا بشه متغیر است و به شرایط و منابع سیستم و وضعیت اپلیکیشن بستگی داره.
  • نوع۲ - Serial Queue: در این نوع صف تضمین داده میشه که در هر زمان فقط و فقط یک task اجرا می‌شه و task ها به صورت متوالی، یکی پس از دیگری اجرا میشن. ترتیب اجرای تسک ها در این نوع صف بر اساس قاعده FIFO می‌باشد. یکی از کاربرد های این نوع صف برای مواقعی است که میخواهید به یک ریسورس مشترک دسترسی داشته باشید تا از تغییر همزمان آن توسط thread های مختلف و بوجود اومدن مشکل race condition جلوگیری کنید.
دقت کنید که شما می‌تونید در هر زمان فقط یک task در هر serial queue اجرا کنید. اما اگر برای مثال چهار صف از نوع serial queue ایجاد کنید و به هر صف یک task اعزام کنید، هر صف می‌تونه به طور مستقل task خودش رو اجرا کنه. در واقع شما دارید چهار task رو در چهار صف مستقل، به طور همزمان اجرا می‌کنید.
  • نوع۳ - Main Queue: این صف نوع جدیدی از صف‌ها نیست بلکه از نوع Serial Queue می‌باشد. فقط به خاطر اهمیتی که داره در این دسته بندی آورده شده. درباره این صف لازمه بدونیم که تنها یک instance ازش برای هر اپلیکیشن ساخته میشه و در سرتاسر برنامه قابل دسترسیه و تمامی task هایی که بهش اعزام میشه رو در main thread یا به قولی همون UI thread اجرا میکنه. به طور کلی هر چیزی که مربوط به UI هست باید توی این صف به اجرا در بیاد.


چند نکته که درباره Dispatch Queue ها باید به خاطر داشته باشید:

  • نکته اول: Dispatch Queue ها task هاشون رو موازی با سایر Dispatch Queue ها اجرا می‌کنن و از هم مستقل هستن.
  • نکته دوم: سیستم، تعداد کل کارهایی که در هر زمان انجام میشن رو تعیین می کنه.
  • نکته سوم: سیستم برای این که تصمیم بیگیره کدوم task ها رو باید بیشتر مورد توجه قرار بده و سریع تر به اجرا درشون بیاره، الویت صف رو در نظر میگیره
  • نکته چهارم: task ها به محضی که به صف اضافه میشن باید آماده اجرا شدن باشن


ساخت DispatchQueue

قبل از اینکه بخواییم یک task رو به یک صف اعزام کنیم باید بدونیم به چه نوع صفی نیاز داریم. همونطور که بالاتر دیدیم، صف ها میتونن task ها رو به صورت موازی یا متوالی اجرا کنن.

الویت ها

وقتی یک صف تشکیل میدیم (چه از نوع serial و چه از نوع concurrent) میتونیم با مقدار دادن به پراپرتی qos که مخفف quality of service هست، اهمیت اجرای صف‌ رو برای سیستم مشخص کنیم. ترتیب الویت ها از زیاد به کم:

.userInteractive .userInitiated .default .utility .background .unspecified
توجه: توی بکار گیری الویت ها دقت کنید و سعی کنید از دادن الویت‌های یکسان به صف ها خودداری کنید چرا که در غیر این صورت الویت دادن بی معنی میشه!

صف موازی یا هم‌زمان - Concurrent Queue

این صف برای مواقعی مناسبه که قصد داریم task ها رو برای اجرا شدن به صورت هم زمان، بهش اعزام کنیم. هر task که زود تر اعزام بشه زودتر اجرا می‌شه (قاعده FIFO).

Concurrent Queue
Concurrent Queue

ما می‌تونیم به راحتی یک صف از نوع موازی رو با استفاده از متد استاتیک global(qos:) در کلاس DispatchQeueu با سطح الویت هایی که در بالا بهشون اشاره شد، ایجاد کنیم(مقدار qos به صورت پیشفرض برابر با default می‌باشد)

Concurrent Dispatch
Concurrent Dispatch

در پایین، خروجی کد بالا رو میبینیم که task ها هم زمان با هم در یک صف از نوع concurrent اجرا شدن:

Concurrent Queue X 0 Concurrent Queue Y 5 Concurrent Queue Z 10 Concurrent Queue Y 6 Concurrent Queue X 1 Concurrent Queue Y 7 Concurrent Queue Y 8 Concurrent Queue Y 9 Concurrent Queue X 2 Concurrent Queue X 3 Concurrent Queue X 4 Concurrent Queue Z 11 Concurrent Queue Z 12 Concurrent Queue Z 13 Concurrent Queue Z 14

حالا بیایید دوتا صف با الویت های متفاوت ایجاد کنیم:

Concurrent Queues With Different Priorities
Concurrent Queues With Different Priorities

همونطور که توی خروجی پایین میبینید، صفی که الویت بالاتری داره، بیشتر مورد توجه سیستم قرار میگیره و با الویت بالاتری نسبت به صف های دیگه اجرا میشه:

High Priority Concurrent Queue 0 Low Priority Concurrent Queue 5 High Priority Concurrent Queue 1 High Priority Concurrent Queue 2 High Priority Concurrent Queue 3 High Priority Concurrent Queue 4 Low Priority Concurrent Queue 6 Low Priority Concurrent Queue 7 Low Priority Concurrent Queue 8 Low Priority Concurrent Queue 9
نکته: هنگام اعزام task ها به یک صفِ موازی، می‌تونید پراپرتی qos رو برای هر task مقدار دهی کنید تا برای صف مشخص بشه موقع اجرای همزمان task ها باهم، به کدوم task اهمیت بیشتری بده.
DispatchQueue.global(qos: .background).async(qos: .userInteractive) { // ... }


صف متوالی - Serial Queue

از این صف در مواقعی استفاده کنید که نیاز دارید task ها به ترتیب و یکی پس از دیگری اجرا بشن. توی این نوع صف تضمینی وجود داره که میگه همیشه در یک زمان فقط یک task در صف در حال اجراست. همچنین برای مواقعی که قصد دارید از یک ریسورسِ مشترک در برابر race condition محافظت کنید، به جای استفاده از DispatchSemaphore و lock کردن ریسورس، می‌تونید از این نوع صف استفاده کنید.

Serial Queue
Serial Queue

برای ایجاد صفِ متوالی، کافیه سازنده کلاس DispatchQueue رو فرواخوانی کنید و به پراپرتی label مقدار دلخواه بدید:(مقدار qos به صورت پیشفرض برابر با default می‌باشد)

Serial Queue
Serial Queue


در خروجی میبینیم که task اول به طور کامل انجام شد و بعد از اون task دوم به اجرا در اومد:

First Task 0 First Task 1 First Task 2 First Task 3 First Task 4 Second Task 5 Second Task 6 Second Task 7 Second Task 8 Second Task 9

انجام task در Main Queue

برای اینکه یک task در main thread اجرا بشه کافیه اون رو به Main Queue اعزام کنیم:

Main Thread
Main Thread


تفاوت sync و async

فرض کنید داخل یک thread (مثلا currentThread) هستید و قصد دارید اجرای یک task رو به یک thread دیگه (مثلا backgroundThread) بسپارید. در این موقع می‌تونید از متد sync یا async استفاده کنید. اما این دو متد یک تفاوت خیلی اساسی دارن. وقتی شما داخل currentThread هستید و متد async رو برای اجرای task در backgroundThread انتخاب می‌کنید، در واقع منتظر تموم شدن task در backgroundThread نمی‌مونید و currentThread به کارش ادامه میده. اما وقتی sync رو صدا می‌زنید، currentThread تا زمان پایان یافتن task در backgroundThread، منتظر باقی می‌مونه.


مراقب dead lock باشید!

Dead Lock
Dead Lock

در کد بالا serialQueue1 و serialQueue2 هر کدوم یک task رو به صورت async اجرا می‌کنن. اما قبل از اینکه task هر کدوم به طور کامل به پایان برسه، هر کدومشون یک task دیگه رو به صورت sync به اون یکی صف اعزام می‌کنه و منتظر میمونه که کار taskای که اعزام کرده به پایان برسه. خب بدیهیه که task هایی که به صورت sync به صف ها اعزام شدن مادامی که صف ها هنوز کار ناتمام دارن، اجرا نمیشن. بنابرین در چنین شرایطی thread های این صف ها به بن‌بست میخورن و اصطلاحا dead lock رخ میده. خروجی این کد رو در پایین میبینیم:

serialQeueu1: 0 serialQueue2: 5 serialQueue2: 6 serialQueue2: 7 serialQeueu1: 1 serialQueue2: 8 serialQeueu1: 2 serialQueue2: 9 serialQeueu1: 3 serialQeueu1: 4



آخرین مطلب - DispatchGroup

گاهی لازم داریم از اتمام کار تمام task هایی که به صورت موازی باهم اجرا کردیم مطلع بشیم تا مطمئن بشیم همه task ها کارشون رو به درستی به پایان رسوندن. مشکل اینجاست که زمان به اتمام رسیدن کار هر task متفاوته. در این مواقع از کلاسی به اسم DispatchGroup استفاده میکنیم. این کلاس سه متد اصلی داره:

enter() leave() notify()

پیش از اجرای هر task متد enter() و پس از پایان کارش متد leave() فراخوانی می‌شه:

DispatchGroup
DispatchGroup

بعد از اینکه به ازای هر بار فراخوانیِ متد enter، متدِ leave فراخوانی شد، ما از طریق متد notify از اتمام کار همه task ها مطلع می‌شیم.


پایان

اگر سوالی داشتین حتما بپرسید.

امیدوارم از خوندن این مقاله لذت برده باشید? ?


مراجع و منابع

https://www.raywenderlich.com/5370-grand-central-dispatch-tutorial-for-swift-4-part-1-2#toc-anchor-004

https://medium.com/@deda9/swift-concurrency-programming-dispatch-queues-9d804cf277f3

swiftiosgcddispatchThread
iOS App Developer
شاید از این پست‌ها خوشتان بیاید