توی این مقاله قصد دارم تا درباره multi-threading در Swift بنویسم و شما رو با GCD و کلاس های مهمی مثل DispatchQueue و DispatchGroup آشنا کنم.
قبل از اینکه بخواییم درباره GCD حرف بزنیم باید یه درک قابل قبولی از threading و concurrency در برنامه نویسی داشته باشیم. پس اگر فکر میکنید که در این باره به اندازه کافی میدونید، میتونید این مرحله رو رد کنید.
توی iOS هر برنامه ای که اجرا میکنید از یک یا چند thread تشکیل شده. اینکه thread ها چطوری و چه زمان باید اجرا بشن، توی پایین ترین لایه، کاملا به عهده سیستم عامل دستگاه میباشد.
دیوایس هایی که تنها یک هسته دارن (single-core devices)، برای انجامِ دو یا چند کار به صورت هم زمان از روشی به اسم time-slicing استفاده میکنن.
حالا این روش چطور کار میکنه؟! فرض کنید دوتا thread داریم که میخواییم به صورت هم زمان، هر کدومشون یه کاری رو برای ما انجام بدن. توی این روش پردازنده خیلی سریع بین thread ها switch میکنه و مقداری از کار هر thread رو انجام میده. به همین ترتیب کار هر دو thread رو همزمان باهم پیش میبره. تصویر زیر نشون میده این کار چطور انجام میشه:
از طرف دیگه دیوایس های چند هسته ای (multi-core devices) رو داریم که میتونن thread ها رو به صورت هم زمان و موازی باهم (parallelism) به اجرا در بیارن.
سه حرف 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 یکی از کلاس های تعریف شده در GCD است که با استفاده از اون میشه به سادگی task هایی رو به صورت همزمان انجام داد. این کلاس صف هایی مستقل از هم تشکیل میده که هر صف وظیفه اجرای task هایی که بهش اعزام میشن رو بر عهده داره. حالا اینکه task های اعزام شده به صف، به صورت متوالی (serial) یا همزمان (concurrent) اجرا بشن، به نوع صفی که تعریف کردیم بستگی داره.
انواع صف ها:
دقت کنید که شما میتونید در هر زمان فقط یک task در هر serial queue اجرا کنید. اما اگر برای مثال چهار صف از نوع serial queue ایجاد کنید و به هر صف یک task اعزام کنید، هر صف میتونه به طور مستقل task خودش رو اجرا کنه. در واقع شما دارید چهار task رو در چهار صف مستقل، به طور همزمان اجرا میکنید.
چند نکته که درباره Dispatch Queue ها باید به خاطر داشته باشید:
قبل از اینکه بخواییم یک task رو به یک صف اعزام کنیم باید بدونیم به چه نوع صفی نیاز داریم. همونطور که بالاتر دیدیم، صف ها میتونن task ها رو به صورت موازی یا متوالی اجرا کنن.
وقتی یک صف تشکیل میدیم (چه از نوع serial و چه از نوع concurrent) میتونیم با مقدار دادن به پراپرتی qos که مخفف quality of service هست، اهمیت اجرای صف رو برای سیستم مشخص کنیم. ترتیب الویت ها از زیاد به کم:
.userInteractive .userInitiated .default .utility .background .unspecified
توجه: توی بکار گیری الویت ها دقت کنید و سعی کنید از دادن الویتهای یکسان به صف ها خودداری کنید چرا که در غیر این صورت الویت دادن بی معنی میشه!
این صف برای مواقعی مناسبه که قصد داریم task ها رو برای اجرا شدن به صورت هم زمان، بهش اعزام کنیم. هر task که زود تر اعزام بشه زودتر اجرا میشه (قاعده FIFO).
ما میتونیم به راحتی یک صف از نوع موازی رو با استفاده از متد استاتیک global(qos:) در کلاس DispatchQeueu با سطح الویت هایی که در بالا بهشون اشاره شد، ایجاد کنیم(مقدار qos به صورت پیشفرض برابر با default میباشد)
در پایین، خروجی کد بالا رو میبینیم که 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
حالا بیایید دوتا صف با الویت های متفاوت ایجاد کنیم:
همونطور که توی خروجی پایین میبینید، صفی که الویت بالاتری داره، بیشتر مورد توجه سیستم قرار میگیره و با الویت بالاتری نسبت به صف های دیگه اجرا میشه:
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) { // ... }
از این صف در مواقعی استفاده کنید که نیاز دارید task ها به ترتیب و یکی پس از دیگری اجرا بشن. توی این نوع صف تضمینی وجود داره که میگه همیشه در یک زمان فقط یک task در صف در حال اجراست. همچنین برای مواقعی که قصد دارید از یک ریسورسِ مشترک در برابر race condition محافظت کنید، به جای استفاده از DispatchSemaphore و lock کردن ریسورس، میتونید از این نوع صف استفاده کنید.
برای ایجاد صفِ متوالی، کافیه سازنده کلاس DispatchQueue رو فرواخوانی کنید و به پراپرتی label مقدار دلخواه بدید:(مقدار qos به صورت پیشفرض برابر با default میباشد)
در خروجی میبینیم که 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 thread اجرا بشه کافیه اون رو به Main Queue اعزام کنیم:
فرض کنید داخل یک thread (مثلا currentThread) هستید و قصد دارید اجرای یک task رو به یک thread دیگه (مثلا backgroundThread) بسپارید. در این موقع میتونید از متد sync یا async استفاده کنید. اما این دو متد یک تفاوت خیلی اساسی دارن. وقتی شما داخل currentThread هستید و متد async رو برای اجرای task در backgroundThread انتخاب میکنید، در واقع منتظر تموم شدن task در backgroundThread نمیمونید و currentThread به کارش ادامه میده. اما وقتی sync رو صدا میزنید، currentThread تا زمان پایان یافتن task در backgroundThread، منتظر باقی میمونه.
در کد بالا 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
گاهی لازم داریم از اتمام کار تمام task هایی که به صورت موازی باهم اجرا کردیم مطلع بشیم تا مطمئن بشیم همه task ها کارشون رو به درستی به پایان رسوندن. مشکل اینجاست که زمان به اتمام رسیدن کار هر task متفاوته. در این مواقع از کلاسی به اسم DispatchGroup استفاده میکنیم. این کلاس سه متد اصلی داره:
enter() leave() notify()
پیش از اجرای هر task متد enter() و پس از پایان کارش متد leave() فراخوانی میشه:
بعد از اینکه به ازای هر بار فراخوانیِ متد enter، متدِ leave فراخوانی شد، ما از طریق متد notify از اتمام کار همه task ها مطلع میشیم.
اگر سوالی داشتین حتما بپرسید.
امیدوارم از خوندن این مقاله لذت برده باشید? ?
https://medium.com/@deda9/swift-concurrency-programming-dispatch-queues-9d804cf277f3