همزمانی در سی++: مفاهیم اولیه
بالاخره شروع کردم به خوندن کتاب C++ Concurrency in action نوشته Anthony Williams.
بالاخره شروع کردم به خوندن کتاب C++ Concurrency in action نوشته Anthony Williams. این کتاب رو باید یک ماه پیش تموم میکردم اما مثل همیشه خیلی کند پیش رفتم و فقط ۲ فصلش رو تا الآن خوندم و تازه دارم خلاصه فصل اول رو مینویسم. به خودم قول دادم که توی این هفته این کتابو تموم کنم و باید انجامش بدم.
این کتاب درباره همزمانی در سی++ عه و راجع به نحوه نوشتن برنامه های این چنینی و چالش های پیش رو و راه های مرسوم برای حل چالش ها صحبت میکنه. یجورایی معروف ترین کتاب در این زمینهست و خود نویسندهش در کمیته استاندارد سی++ در همین بخش کار میکنه و پروپوزال میده.
با اینکه کتاب از مفاهیم پایه شروع به توضیح کرده، به نظرم بهتره آدم قبلش معماری کامپیوتر بدونه تا خیلی راحت تر بفهمه که چی به چیه.
همزمانی(Concurrency) چیست؟
همزمانی درواقع به معنی انجام دادن چندکار در یک زمان واحده. به عنوان مثال میشه به وقتی که داریم راه میریم و همزمان تلفن رو با دستمون نگه داشتیم و داریم چت میکنیم اشاره کرد. اینکار هارو به شکل همزمان انجام میدیم.
همزمانی در کامپیوتر
همونطور که میدونیم، کامپیوتر ها در حالت عادی کار هارو به شکل ترتیبی از دستورات پشت سر هم انجام میدن.
وقتی از همزمانی در کامپیوتر صحبت میکنیم یعنی یک سیستم چندین کار رو به شکل همزمان و موازی انجام بده. حالا میخواد از طریق Task switching باشهیا به معنای واقعی کلمه همزمان باشه.
همزمانی از زمان های خیلی قدیم تر هم بوده. از سیستم عامل های Multi task که میتونستن چندکار رو با استفاده از task switching انجام بدن گرفته تا سرورهایی که چندین پردازنده داشتن و رسما به شکل موازی کار هارو انجام میدادن. اما در دنیای امروز، استفاده از سیستم هایی که به شکل سخت افزاری میتونن وظایف رو به شکل همزمان انجام بدن بسیار بسیار بیشتر شده و به همین دلیله که همه باید بلد باشیم چطور باید با این کامپیوتر ها کار کرد.
همونطور که گفتم، قبلا کامپیوتر ها کلا از دو روش برای انجام همزمان کارها استفاده میکردن:
جابجایی بین وظایف(task switching)
کامپیوتر هایی که یک پردازنده داشتن، از لحاظ تئوری امکان اینکه چند کاررو به شکل همزمان انجام بدن وجود نداشت. پس چطور اینکار رو انجام میدادن؟
با تقسیم کردن وظایف به چند قسمت و انجام دادن یه تیکه از هر وظیفه. یعنی چی؟ یعنی پردازنده در ثانیه بین وظایف مختلف پرش انجام میده و هربار یکم از کار هرکدوم رو انجام میده. اینکار انقدر سریعه که انگار برنامه ها دارن باهمدیگه اجرا میشن و یجور توهم همزمانی رو ایجاد میکنه.
همزمانی سخت افزاری(hardware concurrency)
کامپیوتر هایی که چند پردازنده دارن یا اونایی که یک پردازنده چند هسته ای دارن میتونن واقعا کارها رو به شکل همزمان انجام بدن
درحالت کلی، task switching بسیار کُند تر از همزمانی سخت افزاریه چرا که در حالت اول، پردازنده باید فرآیندی رو به نام _context sw_itchرو انجام بده. خلاصهش اینه که وقتی میخواد از یک کاری به کار دیگه پرش انجام بده، باید state ای که cpu داره رو ذخیره کنه و همچنین اشاره گر به دستوری که میخواسته اجرا کنه رو هم ذخیره کنه. بعد به کار بعدی پرش انجام بده و اونجا state مربوط به اون وظیفه رو بازیابی کنه، دستورات موجود در حافظه رو بارگزاری کنه تا بتونه شروع به پردازش اون وظیفه کنه. همه اینکار ها باعث میشن که تاخیر بیشتری در انجام کارها صورت بگیره. تصویر زیر کاملا این مطلب رو روشن میکنه. دو task داریم که هرکدوم به ۱۰ قسمت تقسیم شدهن(یک task با رنگ قرمز و دیگری سبز).
رویکرد های همزمانی
اساسا از دو چیز برای پیاده سازی همزمانی استفاده میشه:
- ترد ها/ نخ ها/ threads
- پروسه ها/ پردازه ها/ processes
دو کارمند رو تصور کنید که میخوان در یک شرکت روی یک پروژه کار کنن. ایده اول اینه که به هرکدوم یک اتاق جدا بدیم و همچنین یک کپی از راهنمای نرم افزاری که میخوان روش کار کنن هم به هرکدوم بدیم. چالش هایی که روبهرومونه ایناست:
- باید دوتا اتاق رو مدیریت کنیم
- دوتا کارمند برای صحبت کردن با همدیگه مجبورن از جاشون بلند شن و برن پیش همدیگه
- از هر چیزی باید دوتا کپی بگیریم و بهشون بدیم
ایده دوم اینه که هردو نفر رو در یک اتاق بذاریم و اینطوری کافیه یه کپی از راهنمای نرم افزار رو بهشون بدیم. فرق این روش چیه؟
- تمرکز کردن احتمالا براشون سخت تر میشه
- نیاز نیست که چندینکپی از منابع بگیریم اما ممکنه هردوتاشون به یک چیز ثابت نیاز داشته باشن و اینطوری باید صبر کنن تا کار شخص دیگر تموم بشه.
- میتونن به راحتی با هم صحبت کنن و ایده هاشون رو به اشتراک بذارن.
با استفاده از این مثال میتونیم دو رویکرد اساسی رو بررسی کنیم.
کارمند هارو به عنوان ترد ها و اتاق هارو به عنوان پروسه ها در نظر میگیریم.
بنابراین دو رویکرد ما به این صورت خواهند بود:
- چند پروسه تک-نخی
- یک پروسه چند-نخی
چند پروسه تک-نخی
دو پروسه صرفا با استفاده از IPC میتونن باهم ارتباط برقرار کنن و این باعث میشه سرعت اجرای کارها پایین بیاد چرا که سیستم عامل لایه های محافظتیای رو اعمال میکنه که یک پروسه نتونه داده های پروسه های دیگه رو ویرایش کنه یا به راحتی بخونه.
از طرف دیگه اجرای پروسه های زمان بیشتری نیاز داره.
اما خوبی هایی هم داره: اضافه شدن لایه های انتزاعی کمک میکنه که نیاز نباشه برنامه پیچیده ای برای ارتباط نوشته بشه و بیشتر کار رو خود سیستم عامل انجام میده و همچنین، این قابلیت ایجاد میشه که یک وظیفه رو بین چند کامپیوتر در شبکه تقسیم کرد.
یک پروسه چند-نخی
رویکرد دیگه ای که ازش استفاده میشه، نوشتن یک برنامه چند نخیست.
ترد ها یجورایی همون ساده شدهٔ پروسه ها هستن. یک پروسه میتونه از چندینترد تشکیل بشه و بیشتر اطلاعات میتونن از طریق حافظه به اشتراک گذاری شده (shared memory) بین ترد ها جابجا بشن. ترد ها دیگه اون سربار مربوط به محافظت های سیستم عامل رو ندارن به همین دلیل سریعترن و درواقع اکثر برنامهنویس ها از این روش برای پیاده سازی همزمانی استفاده میکنن.
خود سی++ هم راهکار خاصی برای پیاده سازی رویکرد «پروسه های تک-نخی» نداره و برای پیاده سازیش باید به API های سیستم متکی بود.
اما ترد ها یه مشکلی دارن و اونم اینه که باید مطمئن بشیم اون دادهٔ اشتراکی که چند ترد میخوان بخوننش، برای همه ثابت باشه.
راهکار های بسیاری برای حل این مشکل وجود داره که بسته به سناریو میشه ازشون استفاده کرد و این چالش رو هم حل کرد.
تفاوت همزمانی با موازی کاری(Concurrency vs. Parallelism)
یکی از کلماتی که در حوزه برنامه نویسی چند نخی شنیده میشه، موازی کاری یا parallelism است.
این دو مفهوم همپوشانی بسیار زیادی باهم دارن اما با هم متفاوتن.
در حالت کلی هردو مفهوم درباره اجرا کردن چند وظیفه به شکل همزمان و موازی هستن اما هدفشون فرق میکنه.
هدف موازیکاری، افزایش کارایی و performance هست. اینکه سریعتر یک سری پردازش روی داده ها انجام بشه.
هدف همزمانی، جدا کردن بخش های مختلف برنامه یا به عبارت دیگه Separation of Concerns هست.
همزمانی برای جدا کردن بخش های مختلف
اینکه ما بتونیم بخش هایی که به همدیگه مربوطن رو نزدیک هم نگه داریم و بخش هایی که ربطی به همدیگه ندارن رو از هم دور کنیم باعث میشه مدیریت کردنکد و برنامه راحت تر بشه و بخش های مختلف برنامه جدا از همدیگه [و به صورتموازی] کار کنن.
به عنوان مثال یک برنامه DVD Player رو در نظر میگیریم. این برنامه چندین کار رو همزمان باید انجام بده:
- دیکد کردن فایل ویدئویی
- پخش کردن تصویر
- پخش کردن صدا
- ارتباط با کاربر(مثلا برای متوقف کردن پخش)
بدون استفاده از همزمانی، این کار ها نمیتونن به شکل همزمان باهمدیگه انجام بشن (:
همزمانی برای افزایش کارایی
این رویکرد، برای کاهش زمان اجرای الگوریتم بکار میره و دو روش اصلی اون عبارت اند از:
- یک کار ثابت رو به چند قسمت تقسیم کنیم و هر قسمت به شکل موازی باهمدیگه انجام بشن. درواقع به این معنیه که هر ترد یک بخشی از الگوریتم رو انجام بده. به این میگن Task parallelism
- یک داده رو به چند قسمت تقسیم کنیم و یک کار ثابت رو روی قسمت های مختلف داده ها انجام بدیم. به این روش میگن Data Parallelism
الگوریتم های موازی
به الگوریتم هایی که به راحتی میتونن به شکل موازی اجرا بشن اصطلاحا میگن این الگوریتم به شکل خجالت آوری قابل موازی شدنه =)
الگوریتم های embarrassingly parallel این قابلیت رو دارن که به راحتی به چندین قسمت تقسیم بشن و به شکل موازی به سرانجام برسن. همچنین قابلیت مقیاس پذیری دارن یعنی میشه تعداد قسمت هارو به راحتی تغییر داد.
پایان
خیلی موضوع جالبیه و نیازمند مطالعه بسیار. توی کتاب معماری کامپیوتر پترسون هم بهش رسیدم و امیدوارم بتونم وقتمو طوری تنظیم کنم که اونو هم بخونم(خیلی تنبلم).
احتمالا فقط خلاصه بخش هایی رو مینویسم که مفهومی ان و راجع به بخش هایی که بیشتر کد نویسی ان چیزی نمینویسم.
خیلی از بخش های آخر(مثل: «چه زمانی نباید از همزمانی استفاده کنیم؟») رو ننوشتم بخاطر اینکه فکر نمیکنم فراموشم بشن