پیاده سازی Mutex در گولنگ

منبع :)خوب اینجا ما یاد میگیریم چطوری مشکل Race Condition با استفاده از موتکس ها و کانال ها حل کنیم.اول از همه race condition چه کوفتی هست اصلا !؟این مشکل وقتی به وجود میاد که ۲ یا چندین ترد میخواد دسترسی پیدا کنند به دیتاهای توزیع شده و سعی کنند اونارو عوض کنند. خوب نمیشه که فقط یکی میتونه بیاد و یک مقداری تغییر بده اینجاست که conflict شروع میشه.خیلی ساده وقتی یک برنامه وقتی بصورت concurrent داره اجرا میشه. نباید منابع قابل دسترس باشه برای چندین Goroutine.گوروتین چیه !؟گوروتین ها فانکشن ها یا متد هایی هستند که بصورت همزمان با بقیه فانکشن ها و متد ها اجرا میشن. در مقایسه با تردها گوروتین ها بسیار سبک تر و به قولی کم هزینه تر هستند.خوب مثال خیلی ساده ای اینجا شروع میکنیم. فرض کنیم یه کدی داریم که داره مقدار x یک واحد بهش اضافه میکنه و داخل خود x جایگذاری میکنه.x = x + 1تا وقتیکه این کد توسط یک گوروتین قابل دسترس هستش نباید هیچ مشکلی باشه. بیایم بررسی کنیم چرا این کد به مشکل بر میخوره وقتیکه ما چندین گوروتین داریم که دارند بصورت همزمان اجرا میشن. برای اینکه خیلی پیچیده نشده موضوع فرض کنیم ۲ تا گوروتین داریم که دارن کد بالارو همزمان اجرا میکنند.طی ۳ مرحله این کد داره داخل سیستم اجرا میشه مقدار ا فعلی x گرفته میشهمقدار x + 1 محاسبه میشهمقدار محاسبه شده assign میشه به خود xوقتی همه این ۳ مرحله توسط یک گوروتین انجام میشه همه چیز اوکیه و مشکلی نیست.بیاین بررسی کنیم ببینیم چه اتفاقی می افته وقتی ۲ تا گوروتین این کد اجرا میکنن بصورت همزمان. تصویر پایین کاملا نشون میده چه اتفاقی می افته وقتی ۲ تا گوروتین دسترسی پیدا میکنند به x = x +1 بصورت همزمان.ما مقدار اولیه ایکس صفر درنظر گرفتیم. گوروتین ۱ مقدار ایکس میگیره و با ۱ جمعش میکنه و قبل از اینکه بخواد مقدار محاسبه شده به خود ایکس assign کنه گوروتین دوم میاد و مقدار اولیه ایکس که هنوز صفر هستش میگیره و x+1 محاسبه میکنه دوباره سیستم میاد و از موقعیت قبلی گوروتین ۱ شروع میکنه به اجرا شدن و مقدار محاسبه می ریزه داخل ایکس و میشه ۱. حالا گوروتین دوم میاد از ادامه ی مسیر خودش شروع میکنه به اجرا شدن و نهایتا مقدار ایکس بازهم میشه ۱ بعد از اینکه هر دو گوروتین اجرا شدن.حالا یه سناریوی متفاوت بررسی کنیم.همونطور که ملاحظه میکنین گوروتین اول ۳ مرحلرو کامل اجرا میکنه و مقدار ایکس میشه ۱ بعدش گوروتین دوم شروع به اجرا شدن میکنه و بعد از ۳ مرحله مقدار ایکس به ۲ تغییر پیدا میکنه.خوب از این دو سناریو شما فهمیدین که مقدار نهایی ایکس یا ۱ هستش و یا ۲بستگی به نوع Context switching داره، خوب یعنی چی !؟ یعنی اصلا ضمانتی وجود ندارد که ترتیب اجرای اینم گوروتین ها چطوری باشه و اینجاست که ما به Race Condition بر میخوریم :)فقط در یک صورت در سناریو بالا Race Condition میتونه جلوش گرفته بشه اونم وقتیکه یک گوروتین بهش اجازه داده میشه دسترسی داشته باشه به Critical section کد در یک زمان معین. این کار شدنیه با استفاده از موتکس (Mutex)Mutexموتکس مکانیزم قفل کردن برای ما فراهم میکنه و تضمین میکنه که فقط یک گوروتین اجرا کنه کد و از Race Condition جلوگیری خواهد شد.در زبان گو Mutex در پکیج Sync قابل دسترس هستش. دو متد تعریف شده در mutext تحت عنوان Lock و Unlock هر کدی که بین این دو متد اجرا میشه،‌فقط توسط یک گوروتین داره مدیریت میشه بنابراین دیگه Race Condition معنی پیدا نمیکنهmutext.Lock() x = x + 1 mutex.Unlockاگر یک گوروتین قفل شده باشه و یک گوروتین دیگه بخواد تلاش کنه برای قفل شدن اتفاقی که می افته اینه که بلاک میشه تا زمانیکه موتکس به حالت unlock دربیاد اون وقت اجازه پیدا میکنه که کارشو انجام بده.برنامه ای با Race Conditionتوی این بخش ما برنامه ای مینویسیم که مشکل Race Condition داره و توی بخش پایین تر راه حل ها رو مطرح میکنیم :)package main import ( &quotfmt&quot &quotsync&quot ) var x = 0 func increment(wg *sync.WaitGroup) { x = x + 1 wg.Done() } func main() { var w sync.WaitGroup for i := 0; i < 1000; i++ { w.Add(1) go increment(&w) } w.Wait() fmt.Println(&quotfinal value of x&quot, x) }در برنامه بالا فانکشن increment مقدار x یک واحد اضافه میکنه و بعدش ()Done صدا میکنه از WaitGroup برای اینکه بگه کارش تموم شده.ما تعداد ۱۰۰۰ تا گوروتین ایجاد کردیم، هر گوروتین بصورت همزمان اجرا میشه و race condition وقتی بوجود میاد که سعی میکنه مقدار ایکس افزایش بده. تعداد زیادی گوروتین دارن تلاش میکنن که به مقدار ایکس دسترسی پیدا کنن !اگر چندین و چند بار شما این برنامه روی سیستم خودتون اجراش کنین هر بار ممکنه یه عددی نمایش بده مثلا941 یا 928 یا 922 و و وحل مشکل با استفاده از موتکس ما اومدین ۱۰۰۰ تا گوروتین ساختیم و اگر هر کدومشون ۱ واحد به ایکس اضافه کنن مقدار نهایی ایکس باید ۱۰۰۰ بشهpackage main import ( &quotfmt&quot &quotsync&quot ) var x = 0 func increment(wg *sync.WaitGroup, m *sync.Mutex) { m.Lock() x = x + 1 m.Unlock() wg.Done() } func main() { var w sync.WaitGroup var m sync.Mutex for i := 0; i < 1000; i++ { w.Add(1) go increment(&w, &m) } w.Wait() fmt.Println(&quotfinal value of x&quot, x) }میتونین کد اینجا هم اجرا کنینتایپ Mutext از نوع Struct هستش و ما اومدیم متغیر m که صفر هم هستش و از تایپ mutex هست ساختیم و همونطور که میبینین مقدار ایکس که داره تغییر بین فانکشن های Lock و Unlock قرار گرفته و از race condition جلوگیری میشه بخاطر اینکه فقط یک گوروتین اجازه اجرای این کدرو داره در آن واحد.مقدار نهایی الان ۱۰۰۰ خواهد بودیه نکته هست که خیلی مهمه اونم اینه که اینجا go increment(&w, &m) حتما آدرس موتکس باید پاس داده بشه اگر موتکس مقدار پاس داده بشه بجای آدرسش. هر گوروتین یک کپی از موتکس خواهد داشت و race condition لعنتی همچنان پابرجاست.حل مشکل Race Condition با استفاده از کانال هابا استفاده از کانال ها هم ما میتونیم مشکل Race Condition حل کنیم.package main import ( &quotfmt&quot &quotsync&quot ) var x = 0 func increment(wg *sync.WaitGroup, ch chan bool) { ch <- true x = x + 1 <- ch wg.Done() } func main() { var w sync.WaitGroup ch := make(chan bool, 1) for i := 0; i < 1000; i++ { w.Add(1) go increment(&w, ch) } w.Wait() fmt.Println(&quotfinal value of x&quot, x) }کد میتونین اینجا هم اجراش کنینتوی برنامه بالا ما اومده یک Buffered Channel ساختیم با ظرفیت ۱ و اون کانال به فانکشن increment هم پاس داده شده همونطور که میبینین.این آقای buffered channel استفاده شده تا مطمئن باشیم که فقط یک گوروتین دسترسی به افزایش x داره. و این داره هندل با ارسال مقدار true به کانال و بعد از افزایش ایکس مقدار کانال خونده میشه. خوب طی این مرحله چه اتفاقی می افته ؟ تمامی گوروتین هایی که دارن سعی میکنن توی این کانال بنویسن بلاک میشن تا زمانیکه این مقدار از کانال خونده بشه. جالبه نه !؟ باز هم اینجا فقط به یک گوروتین اجازه داده شد که این عملیات انجام بده و Race Condition هندل شد و مقدار نهایی ۱۰۰۰ میشه.تفاوت بین Mutex و Channelsما مشکل Race Condition با استفاده از این دو بزرگوار حل کردیم ! حالا چطوری تصمیم بگیریم از کدوم کجا استفاده کنیم ؟جواب سوال اینه بستگی داره چه مشکلی شما میخواین حل کنین. اگر مشکل شما بیشتر فکر میکنید با موتکس حل میشه خوب برین اجراش کنین و یا برعکس اگر فکر میکنین با کانال قابل حل شدن شک نکنید.اکثر افرادی که تازه کار هستن در گولنگ مشکلات Concurrency اکثرا با کانال ها برطرف میکنن که خیلی فیچر خفن و باحالیه که این زبان در اختیار ما گذاشته. اما این زبان آپشن های دیگه ای هم مثل موتکس در اختیار ما گذاشته در هر صورت استفاده از هر کدوم هیچ مشکلی بوجود نمیاره.در حالت عادی کانال ها استفاده میشن موقعیکه گوروتین ها نیازمند این هستند که در ارتباط با یکدیگر باشند و موتکس برای موقعی که یک گوروتین فقط باید دسترسی داشته باشه به یک بخش critical کد.در مشکلی که ما حل کردیم موتکس بهترین انتخاب چرا چون هیچ نیازی به ارتباط بین گوروتین ها نبوده.پیشنهاد ما اینه که یک ابزار برای مشکلتون انتخاب کنین و به هیچ وجه سعی نکنین مشکل فیت کنید برای یک ابزار !