همزمانی و Multithreading در iOS


همزمانی و Multithreading، یک بخش اساسی در توسعه‌ iOS هستند. در این پست می‌خواهیم مستقیم به سراغ دلیل اهمیت این مبحث رفته و دریابیم که چگونه می‌توان از آن در کد‌ اپلیکیشن‌های cocoa touch خود استفاده کنیم.

همزمانی به معنای رخ‌داد اتفاقات مختلف در زمانی یکسان است.

این مقصود همچنین با استفاده از time-slicing یا قطعه‌بندی زمان یا حالت موازی (parallel) اگر CPUhd با چند هسته در اختیار داشته باشیم، دست یافتنی است. همه‌ی ما تجربه‌ای از نبود همزمانی داشته‌ایم، معمولاً این اتفاق زمانی می‌افتد که یک تسک سنگین موجب فریز شدن اپلیکیشن شود. البته این نکته قابل ذکر است که فریز شدن UI معمولاً با نبود همزمانی رخ نمی‌دهد، ممکن است این اتفاق به دلیل باگ‌‌های نرم‌افزاری نیز رخ دهد، اما اگر نرم‌افزاری از همه توان محاسباتی در دسترسش استفاده نکند ممکن است هنگام نیاز به انجام کار سنگین فریز شود. اگر شما همچین تجربه‌ای داشته باشید ممکن است گزارشی مانند تصویر زیر دریافت کرده‌باشید:

هرآنچه که به I/O فایل، پردازش داده و یا ارتباط شبکه‌ای نیاز داشته باشد، حکم یک بکگراند تسک را دارد(مگر این که شما دلیل قانع کننده‌ای برای متوقف کردن کل برنامه داشته‌باشید). دلایل زیادی برای این‌که این تسک‌ها باید کاربر را از تعامل با بقیه برنامه‌تان محروم کنند، وجود ندارد. ببینید که اپلیکیشن شما چقدر تجربه ‌کاربری بهتری خواهد داشت اگر همچین گزارشی از آنالیزور اپلیکیشن خود دریافت کنید:

آنالیز کردن یک عکس، پردازش یک داکیومنت و یا یک فایل صوتی، یا نوشتن حجم داده‌ی قابل توجه روی دیسک، مثال‌هایی هستند که می‌توان برای انجام آن از مزیت threadهای بکگراند استفاده کرد. بیاید برویم سراغ اینکه ما چگونه می‌توانیم همچین رفتاری را در اپلیکیشن ‌iOS خود پیاده‌سازی کنیم.

یک تاریخچه مختصر
در زمان‌های قدیم، ماکزیمم مقدار کار به ازای هر سیکل پردازنده که یک کامپیوتر می‌توانست انجام دهد را با استفاده از سرعت ساعت اندازه گیری می‌کردند. هرچه طراحی‌های پردازنده‌ها کوچک‌تر شد، گرما و ابعاد فیزیکی یک فاکتور محدود کننده برای سرعت ساعت به حساب آمدند. در نتیجه، تولید کنندگان چیپ شروع به اضافه کردن هسته‌های پردازنده دیگری برروی چیپ‌هایشان کردند تا کارائی کلی را افزایش دهند. با افزایش تعداد هسته‌ها یک چیپ تنها بدون اینکه نیاز به افزایش سرعت، اندازه و یا خروجی گرما، قادر به اجرای دستورات CPU بیشتری در هر سیکل شد. اما یک مشکل وجود داشت...

چگونه می‌توانیم از این هسته‌های اضافه بهره ببریم؟ پاسخ درست Multithreading است.

مفهوم Multhithreading پیاده سازی شده. تا سیستم‌عامل بتواند استفاده و ساخت تعداد نامحدود thread را مدیریت کند. هدف اصلی آن شبیه‌سازی اجرای یک یا چند قسمت از برنامه‌است تا بتواند از تمام زمان دردسترس CPU بهره ببرد. Multhithreading یک تکنیک قدرتمند در جعبه ابزار برنامه‌نویس است که البته مسائل و مشکلات مختص خود را نیز دارد. یک تصور غلط این است که Multhithreading نیاز به یک پرازنده چند هسته دارد، اما این صحیح نیست و پرازنده‌های تک هسته نیز به خوبی قابلیت کار روی threadهای متعدد را دارند، اما ما یک نیم‌نگاهی خواهیم داشت به این که چرا threading در اولین قدم یک مشکل محسوب می‌شود. قبل از شروع به تفاوت‌های ظریفی که بین همزمانی و موازی‌کاری وجود دارد در تصویر زیر نگاهی بیاندازید:

در اولین وضعیت نمایش داده شده در شکل بالا، ما میبینک که تسک‌های می‌توانند به صورت همزمان اما نه به صورت موازی اجرا شوند. این حالت بسیار شبیه وضعیت چت کردن در یک چت روم با چند نفر مختلف است، که شما هربار مجبور می‌شوید موضوع بحث را عوض کنید اما عملاً نمی‌توانید با دو نفر در یک زمان صحبت کنید. این چیزی است که ما به آن همزمانی می‌گوییم. شما را فریب می‌دهد که کار‌های مختلف دارد در یک زمان اتفاق می‌افتد اما حقیقت به این صورت است که خیلی سریع تعویض می‌شوند. همزمانی به معنای سر و کله زدن با تسک‌های زیاد در یک زمان است. کاملا با مدل موازی کاری در تضاد است به غیر ازین که هر دو تسک به صورت همزمان انجام می‌شوند. هر دو مدل اجرایی نمایانگر Multithreading هستند که درگیری threadهای مختلف را برای رسیدن به یک هدف مشترک نشان می‌دهند. Multithreading یک تکنیک تعمیم یافته به منظور استفاده از ترکیب همزمانی و موازی کاری در اپلیکیشن شما است.

بار Threadها

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

به منظور اضافه کردن همزمانی به یک اپلیکیشن نیازمند ساخت یک یا چند thread هستیم. Threadها ساختاری سطح پایین دارند که نیازمند مدیریت دستی هستند. با یک نگاه گذرا به راهنمای برنامه‌نویسی به کمک threadهای اپل به سادگی خواهید فهمید که کدنویسی با استفاده از threadهای چه میزان پیچیدگی به کد شما اضافه خواهد کرد. علاوه بر آن برای ساخت اپلیکیشن یک توسعه‌دهنده باید موارد زیر را نیز انجام دهد:

  • هر thread جدید باید با پذیرش مسئولیت آن ساخته شود و هرمان که وضعیت سیستم تغییر کرد شماره آن به صورت داینامیک تنظیم شود.
  • با دقت مدیریت شوند، هر زمان که اجرای آن‌ها به پایان رسید باید از حافظه آزاد شوند.
  • از مکانیزم‌های همگام‌سازی مانند mutex، قفل و یا سمافور برای هماهنگ سازی منابع قابل دسترس بین threadهای مختلف استفاده شود که خود باعث اضافه شدن بار اضافی رو کد برنامه خواهد شد.
  • برای کاهش ریسک در کدنویسی یک اپلیکیشن به نظر میرسد بیشترین هزینه مربوط به ساخت و نگهداری threadهایی است که استفاده می‌کند و نه سیستم‌عامل میزبان.

باعث تاسف است که این مفهوم بدون اینکه تظمینی برای افزایش کارائی ارائه دهد باعث اضافه شدن سطوح مختلف پیچیدگی و ریسک می‌شود.


مفهوم Grand Central Dispatch

سیستم عامل iOS از رویکرد نامتقارن برای حل مسائل همزمانی در مدیریت threadها استفاده می‌کند. توابع نامتقارن در بیشتر محیط‌های برنامه نویسی مرسوم هستند و اغلب برای ساخت تسک‌هایی که ممکن است کمی زمان‌بر باشند استفاده می‌شود، مانند خواندن یک فایل از دیسک و یا دانلود یک فایل از وب. زمانی که این اتفاق رخ دهد یک تابع نامتقارن یک سری فعالیت در پشت صحنه برای راه اندازی تسک بکگراند انجام می‌دهد اما خیلی سریع بازمی‌گردد و کاری به این که تسک اصلی به چه مقدار زمان نیاز دارد تا کامل شود، ندارد.

یک تکنولوژی بنیادی که iOS برای راه‌اندازی تسک‌های نامتقارن معرفی کرده Grand Central Dispatch یا GCD نام دارد. GCD کد مدیریت thread را خلاصه کرده و آن را به سطح پایین سیستم برده است و یک رابط کاربری سبک برای تعریف و اجرای تسک‌ها روی یک صف اعزام مناسب دارد.

همچنین GCD از تمامی زمانبندی و مدیریت threadها پشتیبانی می‌کند و یک رویکرد جامع برای اجرا و مدیریت تسک دارد و همچنین بازدهی بهتری نسبت به threadهای سنتی دارد.

بیایید نگاهی به اجزای GCD بیاندازیم:

در این تصویر چه می‌بینیم؟ بیاید از سمت چپ شروع کنیم:

  • یک- DispatchQueue.main: این thread اصلی یا همان UI thread است که پشتوانه آن تنها یک صف سریالی است. تمام تسک‌ها به صورت جانشینی اجرا می‌شوند پس می‌توان تضمین کرد که ترتیب اجرا رعایت خواهد شد. بسیار نکته مهمی است که شما اطمینان داشته باشید که تمامی بروزرسانی‌های UI در این صف وارد شوند و شما هرگز نباید تسکی که موجب بلاک شدگی شود را اجرا کنید. ما می‌خواهیم مطمئن شویم که حلقه اجرایی اپلکیشن (به نام CFRunLoop) هرگز بلاک نشده و همچنین بالاترین سرعت اجرا نیز فراهم شود. متعاقباً صف اصلی بالاترین اولویت را دارد و هر تسکی که به آن پوش شود به سرعت اجرا خواهد شد.
  • دو- DispatchQueue.global: بسته مجموعه‌ای از صف‌های همزمانی عمومی که هرکدام به صورت مجزا استخر threadهای خود را مدیریت می‌کنند. بسته به اولویتی که تسک شما دارد، می‌توانید مشخص کنید که در کدام صف قرار بگیرد، اما معمولا در بیش‌تر اوقات بهتر است که از default استفاده کنید. چرا که تسک‌های درون این صف به صورت همزمان اجرا خواهند شد و تضمینی برای حفظ ترتیب تسک‌های درون صف ندارد.

متوجه شدید که ما دیگر با یک thread تنها سروکار نداریم؟ ما با صفی سروکار داریم که استخری از thread‌های داخلی را مدیریت می‌کند و به زودی خواهید دید که چرا صف‌ها رویکرد پایدارتری برای Multithreading هستند.

صف سریالی: Thread اصلی

به عنوان یک مثال نگاهی به تکه کد زیر بیاندازید که زمانی که کاربر دکمه‌ای درون برنامه را بفشارد اجرا خواهد شد. تابع پرهزینه compute می‌تواند هرچیزی باشد. بیاید فرض کنیم آن پس-پردازشی روی یک تصویر ذخیره شده درون دستگاه است.

import UIKit 
class ViewController: UIViewController { 
   @IBAction func handleTap(_ sender: Any) { 
      compute() 
} 
   private func compute() -> Void { 
      // Pretending to post-process a large image.
      var counter = 0 
      for _ in 0..<9999999 {
         counter += 1 
      } 
   } 
}

در نگاه اول شاید به نظر خیلی بی‌خطر باشد اما اگر شما این کد را درون یک اپلیکیشن واقعی اجرا کنید، UI فریز تا زمانی که حلقه به صورت کامل اجرا شود، فریز خواهد شد، که قطعا زمان قابل توجهی طول خواهد کشید. می‌توان این اتفاق را با استفاده از ابزاری اثبات کرد. شما می‌توانید ماژول Time Profiler را در ابزارهای در مسیر Xcode > Open Developer Tool > Instruments در منو Xcode را اجرا کنید. بیایید نگاهی به ماژول Threads در ابزار profile بیاندازیم و ببنید استفاده از CPU به بیشترین حد خود رسیده است.

می‌توانیم ببینیم که thread اصلی به ۱۰۰ درصد ظرفیت خود برای ۵ ثانیه رسیده است. این مدت زمانیست که UI بلاک خواهد شد. به Call Tree زیر چارت نگاه کنید، می‌توان دید که Thread اصلی برای ۴.۴۳ ثانیه از ۹۹.۹ درصد ظرفیت خود استفاده می‌کند! نمایانگر این نکته است ک صف سریال رفتاری FIFO مانند دارد، تسک‌ها همیشه به ترتیبی که وارد می‌شوند، تکمیل خواهند شد. واضح است که متد compute در این مثال مقصر است. آیا می‌توانید تصور کنید که کلیک کردن یک دکمه می‌تواند UI شما را برای این مدت زمان فریز کند؟

مفهوم Threadهای بکگراند

چگونه می‌توان کار بهتری انجام دهیم؟ DispathcQueue.global برای نجات. این جایی است که threadهای بکگراند وارد کار می‌شوند. با ارجا به دیاگرام معماری GCD در بالا ما می‌توانیم ببینیم، هرچیزی که thread اصلی نیست یک thread بکگراند محسوب می‌شود. آن‌ها می‌توانند کنار thread اصلی اجرا شوند، می‌توان ‌آن را کاملا پر کرد و تمام اتفاقات UI مانند اسکرول کردن یا انیمیشن و پاسخ به تعامل کاربر و غیره را به راحتی مدیریت کرد. بیایید یک تغییر کوچک به تابع button clickمان در کد بالا بدهیم:

class ViewController: UIViewController { 
   @IBAction func handleTap(_ sender: Any) {
      DispatchQueue.global(qos: .userInitiated).async { [unowned self] in 
         self.compute() 
      } 
   } 
   private func compute() -> Void { 
      // Pretending to post-process a large image. 
      var counter = 0 
      for _ in 0..<9999999 {
         counter += 1
      } 
   } 
}

بدیهی است که یک تکه کد به صورت پیش فرض روی صف اصلی اجرا خواهد شد، پس ما باید آن را مجبور کنیم تا روی یک thread دیگر اجرا شود، ما compute را درون یک closure نامتقارن (async) قرار می‌دهیم و مشخص می‌کنیم که در صف DispatchQueue.global قرار بگیرد. به خاطر داشته باشید که ما در این‌جا قصد مدیریت threadها را نداریم. ما تسک‌ها را درون یک صف مناسب (در قالب closure یا بلاک) با فرض این که تضمین می‌کند تا در یک نقطه زمانی اجرا شود، قرار می‌دهیم. صف مورد تظر تصمیم می‌گیرد تا چه threadای به تسک تعلق بگیرید و تمام کار سخت ارزیابی نیازمندی‌های سیستم و مدیریت thread واقعی را انجام می‌دهد. این جادوی Grand Central Dispatch است. همانطور که در ضرب‌المثل‌های قدیمی آمده، شما نمی‌توانید چیزی که نمی‌توانید اندازه بگیرید را ارتقا دهید. خب ما توانستیم کلیک button فاجعه‌بارمان را ارزیابی کنیم و حالا آن را ارتقا دادیم، یک بار دیگر آن را ارزیابی می‌کنیم تا اطمینان پیداکنیم که چقد کارائی را بالا برده‌ایم.

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

شما ممکن است متوجه این قضیه شده باشید که ما به صف عمومی با اولویت userInitiated دسترسی داده‌ایم. این attributeای است که ما می‌توانیم هنگام نیاز فوری به تسکمان بدهیم. اگر ما بتوانیم همان تسک را در صف عمومی انجام دهیدم و attribute کیفی بکگراند را به آن بدهیم، iOS فکر میکند که آن یک تسک utility است و منابع کمتری برای اجرا به آن اختصاص می‌دهد. پس ما کنترلی روی این موضوع که تسکمان چه زمانی انجام می‌شود نخواهیم داشت، ما روی اولویت آن کنترل داریم.


نکته‌ای در رابطه با thread اصلی و صف اصلی

شما ممکن است تعجب کنید که چرا Profiler دارد thread اصلی را نشان می‌دهد. و ما چرا به آن صف اصلی می‌گوییم. اگر نگاهی به معماری GCD بیاندازید در آنجا توضیح دادیم که تنها وظیفه صف اصلی مدیریت thread اصلی است. بخش صف اعزام در راهنمای برنامه نویسی با همزمانی می‌گوید که صف اعزام اصلی صف سریالی و عمومی همیشه دردسترس است که تسک‌ها را در thread اصلی برنامه اجرا می‌کند. از آن‌جایی که روی thread اصلی برنامه شما اجرا می‌شود، صف اصلی معمولاً به عنوان کلید نقطه هماهنگ‌سازی برای یک اپلیکیشن استفاده می‌شود.

واژه اجرا روی thread اصلی و اجرا روی صف اصلی می‌توانند به جای یکدیگر استفاده شوند.


صفوف همزمانی

تا اینجا فهمیدیم که تسک‌های ما منحصراً با رفتار سریالی اجرا می‌شوند. DispatchQueue.main به صورت پیش‌فرض یک صف سریالی است و DispatchQueue.global به شما چهار صف اعزام همزمان بسته به پارامتر اولویتی که شما به آن پاس می‌دهید، ارائه می‌دهد.

بیایید فرض کنیم ما پنج تصویر داریم و می‌خواهیم که اپلیکیشن‌مان آن‌ها را به صورت موازی در thread بکگراند پردازش کند. چگونه باید این کار را انجام دهیم؟ ما می‌توانیم یک صف همزمانی با مشخصه‌ای به انتخاب خودمان ایجاد کرده و تسک‌هایمان برای اجرا به آن اختصاص بدهیم. تمام چیزی که نیاز داریم attributeای با نام concurrent هنگام ساختن صف است.

class ViewController: UIViewController {
   let queue = DispatchQueue(label: &quotcom.app.concurrentQueue&quot, attributes: .concurrent) 
   let images: [UIImage] = [UIImage].init(repeating: UIImage(), count: 5) 
   @IBAction func handleTap(_ sender: Any) { 
      for img in images {
            queue.async { [unowned self] in
                self.compute(img) 
            } 
      } 
   } 
   private func compute(_ img: UIImage) -> Void {
      // Pretending to post-process a large image. 
      var counter = 0 
      for _ in 0..<9999999 {            
         counter += 1 
      } 
   }
}


وقتی این کد را با استفاده از ابزار Profiler بررسی کنیم، می‌بینیم که اپلیکیشن ما حالا ۵ thread مجزا را به صورت موازی برای حلقه مورد نظر اجرا می‌کند.

موازی سازی N تسک

تا به الان ما دیدم که چگونه می‌توان تسک یا تسک‌هایی که از نظره هزینه محاسباتی گران هستند را وارد thread بکگراند کنیم بدون اینکه‌ دچار گرفتگی در UI شویم. اما نظرتان در مودر اجرای تسک‌های موازی با مقداری محدودیت چیست؟ چگونه اپلیکیشن Spotify آهنگ‌های مختلف را به صورت موازی دانلود می‌کند یا چرا تعداد دانلود همزمان را به ۳ دانلود محدود کرده است؟ ما می‌توانیم برای فهم این مساله از راه‌های مختلفی شروع کنیم، اما مفید است تا یک ساختار مهم دیگر را در برنامه‌نویسی multithread بشناسیم به نام سمافور.

سمافورها مکانسیم سیگنالی دارند. آن‌ها معمولاً برای کنترل دسترسی به منابع مشترک استفاده می‌شوند. سناریویی را فرض کنید که threadای می‌تواند دسترسی به یک تکه کدی که اجرا می‌کند را قفل کند و بعد از این‌که انجام شد آن را آنلاک کند و به بقیه thread‌ها اجازه بدهد تا آن را اجرا کند. این نوع رفتار را معمولاً در خواندن و نوشتن در دیتابیس مشاهده می‌کنید، برای مثال اگر بخواهید عملیات نوشتن در دیتابیس را فقط یک thread انجام دهد و دیگر threadها اجازه خواندن در آن زمان را نداشته باشند، چه کاری انجام می‌دهید؟ این یک مساله در امنیت thread است که قفل نویسنده و خواننده نام دارد. سمافور‌ها می‌توانند برای کنترل همزمانی در اپلیکیشن‌مان استفاده شوند که این‌کار را با قفل کردن N تعداد thread انجام ‌می‌دهند.

let kMaxConcurrent = 3 // Or 1 if you want strictly ordered downloads! 
let semaphore = DispatchSemaphore(value: kMaxConcurrent) 
let downloadQueue = DispatchQueue(label: &quotcom.app.downloadQueue&quot, attributes: .concurrent) 
class ViewController: UIViewController { 
   @IBAction func handleTap(_ sender: Any) { 
      for i in 0..<15 {            
         downloadQueue.async { [unowned self] in 
            // Lock shared resource access                
            semaphore.wait() 

            // Expensive task 
            self.download(i + 1)
 
            // Update the UI on the main thread, always! 
            DispatchQueue.main.async {                    
               tableView.reloadData() 

               // Release the lock                    
               semaphore.signal() 
            } 
         } 
      } 
   } 
   func download(_ songId: Int) -> Void { 
      var counter = 0 

      // Simulate semi-random download times. 
      for _ in 0..<Int.random(in: 999999...10000000) {            
         counter += songId         
      } 
   } 
}

دیدیم که چگونه می‌توان به طور موثر سیستم دانلودمان، خود را به K تعداد دانلود محدود کند. زمانی که یک دانلود تمام شود (یا اجرای thread به اتمام برسد)، مقدار سمافور را کاهش داده و اجازه مدیریت صف را برای ایجاد یک thread دیگر صادر می‌کند تا یک آهنگ جدید شروع به دانلود شود. شما می‌توانید الگوی مشابهی را برای تراکنش‌های دیتابیس زمانی که با خواندن و نوشتن‌های همزمان سر و کار دارید، استفاده کنید.

سمافور‌ها معمولاً برای کد‌هایی مانند مثالمان ضروری نیستند، اما آن‌ها زمانی که نیاز داشته باشند تا یک رفتار متقارن هنگام استفاده از یک API نامتقارن داشته باشید، خیلی قدرتمند ظاهر می‌شوند. مثال بالا نیز می‌تواند به خوبی با استفاده از NSOperationQueue به همراه یک maxConcurrentOperationCount انجام شود،‌ اما ارزشش را داشت که راه حل متفاوتی را نیز در نظر داشته باشیم.


کنترل بهتر با OperationQueue

مفهوم GCD زمانی که بخواهید یک تسک یکبار مصرف یا closureای را به یک صفی با مدل انجامش بده و فراموشش کن، اعزام کنید، عالی هستند و یک روش بسیار سبک برای این‌کار فراهم می‌کنند. اما چگونه می‌توان، یک تسک طولانی مدت ساختاریافته و قابل تکرار تولید کنیم که یک وضعیت یا دیتای مرتبط ایجاد میکند را بسازیم؟ و این که چطور می‌توان مدلی داشت که شامل زنجیره‌ای از عملیاتی باشد که بتوان آن‌ها را کنسل، معلق و ردیابی کرد و همچنان با یک API که closure دوست باشند کار کند؟ یک عملیات مانند تصویر زیر را تصور کنید:

این پیاده‌سازی همچین چیزی با استفاده از GCD کمی به مشکل خواهیم خورد. ما یک راه ماژولارتری برای تعریف گروهی از تسک‌ها می‌خواهیم که همزمان بتوانیم از خوانا بودن آن‌ها اطمینان حاصل کنیم و همچنین کنترل بیش‌تری روی آن‌ها داشته باشیم. در این حالت، ما می‌توانیم از آبجکت‌های Operation استفاده کنیم و آن‌ها را در یک OperationQueue به صف کنیم، که یک wrapper سطح بالا دور DispatchQueue است. بیایید نگاهی به مزایای استفاده از این روش بیاندازیم و ببینیم که در مقایسه با API سطح پایین GCI چه پیشنهادی دارد:

  • شاید شما بخواهید وابستگی‌هایی بین تسک‌ها ایجاد کنید و زمانی ‌که می‌خواهید این‌کار را با GCD انجام دهید بهتر است که آن‌ها را به عنوان آبجکت‌‌های Operation تعریف کنید یا واحد کار و آن‌ها را به صف خود انتقال دهید. این کا‌ر به شما ماکزیمم قابلیت استفاده مجدد را تا زمانی که بخواهید آن را در الگوی مشابهی هرجای دیگر اپلیکیشن استفاده ‌کنید، می‌دهد.
  • کلاس‌های Operation و OperationQueue اجزای قابل مشاهده دارند که از KVO (مشاهده بار اساس key/value). این یک مزیت مهم دیگریست که می‌توانید وضعیت operation یا صف آن را مشاهده کنید.
  • در این حالت Operationها می‌توانند pause، resume و کنسل شوند. زمانی که شما بخواهید توسط GCD یک تسک را اعزام کنید پس از آن هیچ کنترلی روی اجرای آن تسک نخواهید داشت. Operation API از این لحاظ بسیار انعطاف‌پذیرتر است و به توسعه‌دهنده کنترل روی سیکل زندگی operation را می‌دهد.
  • صف Operation به شما اجازه می‌دهد تا ماکزیمم تعداد Operationهای داخل صف که می‌توانند همزمان اجرا شوند را مشخص کنید، و به شما قابلیت بیشتری برای کنترل روی جنبه‌های همزمانی خواهد داد.

استفاده از Operation و OperationQueue می‌تواند یک پست خالی وبلاگ را کامل پر کند یعنی خیلی طولانی خواهد بود اما بیایید نگاهی به مثالی برای این‌که دریاببیم مدلسازی وابستگی‌ها در این حالت به چه صورت است بیاندازیم. (GCD هم وابستگی‌های ایجاد می‌کند، اما بهتر است که تسک‌های بزرگ را به یک سری زیرتسک‌های قابل ترکیب تقسیم‌بندی کنید.) برای ساخت زنجیره‌ای از operationهای که به یکدیگر وابسته‌اند، می‌توانیم به صورت زیر عمل کنیم:

class ViewController: UIViewController { 
   var queue = OperationQueue() 
   var rawImage = UIImage? = nil 
   let imageUrl = URL(string: &quothttps://example.com/portrait.jpg&quot)! 
   @IBOutlet weak var imageView: UIImageView! 

   let downloadOperation = BlockOperation { 
      let image = Downloader.downloadImageWithURL(url: imageUrl) 
      OperationQueue.main.async { 
         self.rawImage = image         
      } 
   } 

   let filterOperation = BlockOperation { 
      let filteredImage = ImgProcessor.addGaussianBlur(self.rawImage) 
      OperationQueue.main.async { 
         self.imageView = filteredImage         
      } 
   }     

   filterOperation.addDependency(downloadOperation) 

   [downloadOperation, filterOperation].forEach {
   queue.addOperation($0) 
   } 
}

خب پس چرا ما از یک انتزاع سطح بالاتر استفاده نکنیم و به کل از GCD دوری کنیم؟ چون GCD تنها برای پردازش‌های نامتقارنی خطی ایده‌آل هستند، Operation یک مدلی شی‌گرا و جامع‌تر برای محاسبات کپسوله‌سازی تمام دیتا‌ی ساختار‌یافته و تسک‌های قابل تکرار در یک اپلیکیشن ارائه می‌دهد. توسعه‌دهندگان باید از بالاترین سطح انتزاع ممکن برای مسائلی از قبیل کارهای تکرار شونده و سازگار با زمانبندی استفاده کنند که این انتزاع Operation نام دارد. در جای دیگر اگر نیاز به اجرای تسک‌های یکبارمصرف یا closure بود، استفاده از GCD بسیار منطقی به‌نظر می‌رسد. همچنین ما می‌توانیم هردوی GCD و OperationQueue را باهم ترکیب کنیم و از مزایای هر دو استفاده کنیم. برای مثال کاملاً امکان پذیر است نا ۱۰۰۰۰ تسک بسازید و آن‌ها را در یک صف قرار دهید، اما با این‌کار به طور غیر مسقیم فضای حافظه را میگیرید و باعث بوجود آمدن سربار به ازای هر تخصیص و گرفتن بلاک‌های Operation خواهید شد. این دقیقاً در تضاد با آنچه که به دنبالش هستید، می‌باشد! بهترین کار بررسی اپلیکیشن برای اطمنیان از این‌ است که آیا با این کار عملکرد اپلیکیشن را بهبود داده‌اید یا خیر.


هزینه همزمانی

صف اعزام و دوستانش به منظور راحت‌تر کردن اجرای کد به صورت همزمان برای توسعه‌دهندگان اپلیکیشن معرفی شده‌اند. البته این تکنولوژی‌ها هیچ تضمینی برای بهبودی در کارائی یا پاسخگويی در یک اپلیکیشن ارائه نمی‌دهند. این امر بستگی به شما دادر تا از یک صف با رفتاری استفاده کنید که هم عملکرد خوبی داشته باشد و هم تحمیل ناعادلانه‌ای روی دیگر منابع نداشته باشد. برای مثال این امر بسیار امکان‌پذیر است که شما ۱۰۰۰۰ تسک بسازید و آن‌ها را درون یک صف قرار دهید، اما با این‌کار شما مقدار قابل توجهی از حافظه را به آن‌ها اختصاص داده‌اید و سربار زیادی را برای تخصیص و آزادسازی بلاک‌های عملیات به سیستم تحمیل خواهید کرد. این دقیقاً در تضاد با خواسته‌ی شماست. بهترین راه‌کار این است که شما اپلیکیشن خود را آنالیز کنید تا از این‌که همزمانی عملکرد آن را بهبود داده یا تاثیر منفی داشته اطمینان حاصل کنید.

ما در رابطه با این‌که که چطور همزمانی با خود هزینه‌هایی از قبیل پیچیدگی و تخصیص منابع سیستم را دربر دارد، صحبت کردیم، اما همزمانی ریسک‌های دیگری نیز همراه خود خواهد داشت:

  • بن‌بست: وضعیتی اس که یک thread بخش مهمی از کد را قفل می‌کند و می‌توای حلقه‌ی اجرای برنامه را کاملاً از کار بیاندازد. هنگام استفاده از GCD باید زمان استفاده از صدازدن‌های dispatchQueue.sync بسیار مراقب بود چرا که به سادگی می‌توند شما را در وضعیتی قرار دهد که دو عملیات همزمان در انتظار یکدیگر گیر کنند.
  • وارونگی اولویت: به وضعیتی گفته می‌شود که یک تسک با اولیت پایین جلوی اجرای تسکی با اولویت بالاتر را بگیرد. GCD به شما اجازه می‌دهد تا برای دو صف بکگراند اولویت‌های متفاوتی تعریف کنید پس برای حل این مشکل راه حل ساده‌ای وجود دارد.
  • مشکل تولید‌کننده - مصرف‌کننده: یک وضعیت رقابتی بوجود آمده در زمانی که یک thread دیتای منبعی را می‌سازو و thread دیگری به آن دسترسی دارد. این یک مشکل هماهنگ‌سازی است و اگر شما از صف همزمانی در GCD استفاده کنید، می‌توانید با استفاده از لاک‌، سمافور، صف سریالی یا یک جلوگیری از اعزام برطرف شود.
  • و شاید موارد دیگری از وضعیت‌های قفل‌شدگی و رقابت‌داده‌ای که دیباگ کردن آن‌ها مشکل باشد! thread امن بودن بزرگترین نگرانی‌ است که هنگام استفاده از همزمانی خواهید داشت.

مطالعه بیش‌تر

خوشبختانه این مقاله به شما پیش‌زمینه‌ای در رابطه با تکنیک‌های multithreading در iOS را می‌دهد و شما آموزش می‌دهد که چگونه از آن‌ها در اپلیکیشن خود استفاده کنید. ما قرار نیست که بیش‌تر ساختار سطح پایین مانند locks، mutex و این‌که چگونه آن‌ها برای دستیابی به هماهنگ‌سازی به ما کمک‌می‌کنند را شرح دهیم و همچنین نمی‌خواهیم مثال‌های پیچیده‌ای در رابطه‌ با این که چقدر همرمانی ‌می‌تواند به اپلیکیشن شما آسیب برساند،‌ بزنیم. این دسته موارد را برای بعد می‌گذاریم و اگر تمایل داشته باشید می‌توانید موارد زیر را مشاهده کنید.

منبع اصلی مقاله : https://www.viget.com/articles/concurrency-multithreading-in-ios/