کار با متغیرها در iOS از چند رشته (Multi-thread) - بخش ۱

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

امروزه به ندرت میشه اپی نوشت که چند رشته‌ای نباشه. کافیه برنامه بخواد چیزی را از شبکه دریافت کنه یا یه فایل حجیم را از روی دیسک بخونه. اگر بخوایم ترد اصلی را منتظر تکمیل بذاریم صفحه‌ی برنامه برای چند ثانیه هنگ میکنه. چیزی که برای برنامه‌های جدید و با وجود GUI قابل تحمل نیست.

یه برنامه‌ی خوب، به نحو احسن از قابلیت چند رشته‌ای استفاده می‌کنه. روی پلتفرم اپل، فریمورک Dispatch خلق و کار با سیستم چند رشته‌ای را بسیار ساده می‌کنه. ولی یه مشکل بزرگ به وجود میاد، کار با حافظه‌ی قابل تغییر و به طور دقیقتر، متغیرهای mutable! (در سوییفت، var ها)

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

تعریف Race یعنی مسابقه‌ی اسب سواری و condition یعنی حالت. بعبارتی یعنی حالتی که دو تا رشته قصد دارن به صورت همزمان به خط پایان یا همون حافظه‌ی متغیر برسن. این که چه کسی برنده می‌شه قابل پیش‌بینی نیست. در حالی که عموما انتظار داریم اولی برنده بشه، ولی ممکنه دومی برنده بشه و تمام معادلات را بهم بزنه! خود Race condition گاهی اینقدر جدی هست که می‌تونه منجر به حفره‌های امنیتی بشه. برای همین اپل یه قسمتی از داکیومنت‌های امنیتی‌ش را به این موضوع اختصاص داده.(البته برای فایل روی دیسک هست. و ربطی به بحث ما نداره)

پیدا کردن Race condition هم معضل بزرگی هست (البته نه با روشی که خواهم گفت). ممکنه اسبی که روش شرط بستیم هزار بار برنده شه و اون یکی فقط یکبار، یا اینکه برنده‌ای نباشه! بنابراین توی تست‌ها چیزی معلوم نمیشه. ولی برای کاربر نهایی ممکنه باعث کرش بشه و کاربر را شاکی کنه. (در واقع اگر کرش بکنه، شانس آوردید که برنامه در حالت ناپایدار ادامه پیدا نکرده!)

برای اینکه بفهمید چقدر پیش‌بینی ناپذیر هست، کد زیر را روی playground سوییفت چند دفعه اجرا کنید و ببینید که گاهی بعد تعداد نامعلومی کرش میکنه و گاهی هم بدون مشکل اجرا می‌شه!

var a: [Int] = []
let iterations = 2000
DispatchQueue.concurrentPerform(iterations: iterations) { index in
    a.append(index)
}

این مشکل باعث شده که برخی زبان‌های برنامه‌نویسی مثل Lisp یا Haskell متغیرهای mutable را تقریبا بذارن کنار. ولی سوییفت بعنوان یه زبان انعطاف‌پذیر، صرفا این را توصیه می‌کنه.

اما گاهی چاره‌ای نیست. حالا قدم اول اینه که بفهمیم کدوم متغیرهای ما درگیر این موضوع هستن و باید روشون کار کنیم. به طور کلی هر متغیر mutable که از چند رشته دسترسی خواندن/نوشتن داره باید بررسی بشه. گاهی هم متدهای کلاس/استراکت ما که mutating هستن باعث ایجاد مشکل می‌شن.

اینجا ۲ تا تعریف داریم. یکی Thread-safe هست، یعنی کلاسی که این امکان را می‌ده که از چند رشته همزمان متدهاش اجرا بشه و مشکلی پیش نیاد. فی الواقع خودش با روشهایی که بعدا میگم مشکل را حل می‌کنه. اکثر کلاس‌های پایه‌ای مثل Array/NSMutableArray و Dictionary/NSMutableDictionary و String/NSMutableString ترد-سیف نیستن و باید احتیاط کنید. ولی اگر در داکیومنت اپل نوشتن باشن که هست، یعنی نگران نباشید. کلاس‌های NSFileManager، NSURLSession و NSCache از جمله کلاسهای ترد-سیف هستن. در مورد کلاس UIImage هم احتیاطاتی هست.

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

پیدا کردن مظنون‌ها

کامپایلر Clang به طبعش Xcode امکانی دارن برای یافتن متغیرهایی که می‌تونن باعث ایجاد مشکل بشن و هنوز thread-safe یا atomic نیستن.

اول دیوایس را یکی از انواع سیمولاتور (و نه دیوایس واقعی) انتخاب کنید. از توی Xcode برید توی منوی Product -> Scheme -> Edit Scheme و صفحه‌ی زیر را بیارید

صفحه‌ی Edit Scheme
صفحه‌ی Edit Scheme

حالا اون قسمتی که قرمز کردم را مارک بزنید.

در واقع الان داریم از امکانی استفاده می‌کنیم به اسم Thread Sanitizer یا TSan که توسط گوگل توسعه داده شده و البته به لطف اپل روی سوییفت هم قابل استفاده ست. این گزینه فقط وقتی فعاله که روی سیمولاتور باشیم.

حالا برنامه را اجرا می‌کنیم و به قسمت‌های مختلفش میریم. چون این مشکل/ارور از نوع ران-تایم هست، حتما باید کد قسمت‌هایی که به متغیر دسترسی پیدا می‌کنن حداقل یک مرتبه اجرا شه.

اگر کد ما مشکل داشته باشه، اروری توی قسمت زیر ظاهر میشه. همچنین معلوم می‌کنه از کجا‌ها داره دسترسی همزمان به متغیر صورت می‌گیره.

خب اگر خوب به ارور بالا دقت کنیم، هر کدوم از ارورها یه قسمت Read داره یه قسمت Write و یه قسمت Heap allocation. قسمت Read میگه از کدوم ترد در حال خوندن مقدار متغیر هستیم، قسمت Write هم که معلومه. قسمت allocation هم میگه توی کدوم ترد شی را ایجاد کردیم.

همونطور که میبینید ترد ۱۰ داره میخونه ولی ترد ۵ داره مینویسه. زیر هر کدوم یه call stack داریم که میگه دقیقا از کجا به متغیر بی محابا دسترسی پیدا می‌کنیم و اگر روشون بزنید می‌پره روی خط کد. البته برای ما صرفا مهم هست که کدوم متغیر این مشکل را داره.

ایشالله در مقاله بعدی توضیح می‌دم چه جوری باید این متغیر مشکل‌دار، اتمیک بشه و این معضل را حل کنیم.