پیاده سازی 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 کد.

در مشکلی که ما حل کردیم موتکس بهترین انتخاب چرا چون هیچ نیازی به ارتباط بین گوروتین ها نبوده.

پیشنهاد ما اینه که یک ابزار برای مشکلتون انتخاب کنین و به هیچ وجه سعی نکنین مشکل فیت کنید برای یک ابزار !