Concurrency in GO

نام کتاب: کانکارنسی در گو
ویرایش: اول
سال چاپ:۱۳۹۷
سال خوندن من: ۱۴۰۴ تابستان
1. An introduction to Concurrency
چرا اصلا کانکارنسی سخته چون معمولا غیر قابل پیش بینی هستی و پیدا کردن مشکل سخت. فکر کن شما یه کد اسینکی نوشتی که درست کار میکنه ولی بعد ۵ سال یه تسک در پس و پیش اون کده اضافه میشه که باعث ترکیدن کد قبلی میشه
Race Condition
کلا این پدیده زمانی اتفاق میفته که دو یا چند عملیات باید توی ترتیب درست اجرا شن که برنامه درست کار کنه ولی این شرایط و ترتیب همیشه گارانتی نمیشن. مثلا کد زیر:
var data int
go func() {
data++ // 1
}()
if data == 0 { // 2
fmt.printf("the value is %v \n", data) // 3
}
سه تا حالت داره
- اول ۲ اجرا میشه بعد ۳ و بعد ۱
- اول ۱ اجرا میشه بعد ۲ و بعد ۳
- اول ۲ اجرا میشه بعد ۱ و بعد ۳ دقیقا مشکل کد کانکارنت همینه موقع نوشتن کد باید همه سناریو های لازم در نظر بگیریم. حالا اگه بیایم این کار رو بکنیم چی:
var data int
go func() {
data++ // 1
}()
time.sleep(1*time.second)
if data == 0 { // 2
fmt.printf("the value is %v \n", data) // 3
}
ما با تاخیری که اضافه کردیم فقط احتمال وقوع رو کم کردیم ولی مشکل همچنان هست
Atomicity
وقتی میگیم یه چیزی اتمیک هستش یعنی در اون کانتکستی که اجرا میشه غیرقابل تقسیم و غیرقابل وقفه است. یا همه اش رو انجام بده یا هیچی. چیزی که اینجا مهمه کانتکست هستش، یک چیزی میتونه در یک لایه اتمیک باشه ولی در لایه های دیگر اتمیک نباشه
مثلا این دستور ساده ++i
میتونه اتمیک باشه یا نه بستگی به کانتسک ما داره
- دریافت مقدار i
- افزایش مقدار i
حالا ممکنه هر کدوم از این مراحل در سطح CPU به تنهایی اتمیک باشن،
ولی ترکیبشون با هم ممکنه اتمیک نباشه،
مخصوصاً اگه چند تا Thread یا Process همزمان بخوان این دستور رو اجرا کنن.
یا فرض کن داریم یه رکورد جدید توی یه جدول دیتابیس اضافه میکنیم:
INSERT INTO users (id, name, balance) VALUES (1, 'Ali', 100);
در سطح دیتابیس این دستور اتمیکه. یعنی یا کل رکورد users
با همه ستونها وارد میشه، یا اگه خطایی پیش بیاد هیچچیزی وارد نمیشه. پس تو این لایه، این عملیات اتمیک هست.
حالا فرض کن ما از دیتابیسمون از طریق یه API استفاده میکنیم و چند تا سرور داریم. حالا دو کاربر تقریباً همزمان درخواست میفرستن برای ساخت یوزر با همون id = 1
.
اگه ما در سطح اپلیکیشن قبل از INSERT یه چک بکنیم که «آیا کاربری با این id وجود داره؟» و بعد تصمیم بگیریم که رکورد رو درج کنیم، ممکنه این سناریو پیش بیاد:
-
اThread A بررسی میکنه و میبینه که id = 1 وجود نداره
-
همزمان Thread B هم همین کار رو میکنه
-
هر دو به این نتیجه میرسن که باید رکورد درج بشه
-
هر دو همزمان سعی میکنن INSERT بزنن
-
یکی موفق میشه، یکی خطا میگیره
Memory Access Synchronization
وقتی چند goroutine دارن به یه تکه حافظه (مثلاً یه متغیر مشترک) همزمان دسترسی پیدا میکنن، باید یه جوری هماهنگشون کنیم که داده خراب نشه. به این هماهنگی برای دسترسی به حافظه میگیم Memory Access Synchronization. حالا اگه اینارو مدیریت نکنیم ممکنه چندتا حالت پیش بیاد
DeadLock
ددلاک وقتی اتفاق میافته که چند تا goroutine منتظر همدیگه باشن و هیچکدوم نتونه کارشو ادامه بده. مثلا فرض کن دو تا قفل داریم. goroutine اول قفل A رو میگیره و منتظره قفل B آزاد بشه، همزمان goroutine دوم قفل B رو گرفته و اونم منتظره قفل A. هیچکدوم حاضر نیستن قفل رو ول کنن، پس برنامه همینجوری گیر میکنه و هیچ اتفاقی نمیافته.
Livelock
لایولاک شبیه deadlock هست ولی یه تفاوت داره. تو deadlock همه متوقف شدن، ولی تو livelock همه دارن حرکت میکنن، فقط هیچکدوم به نتیجه نمیرسن. انگار دو نفر روبروی هم ایستادن، هر دو میخوان کنار برن تا راه بدن، اما هر بار همزمان حرکت میکنن و دوباره جلوی هم قرار میگیرن. این چرخه همینجوری تکرار میشه و در نهایت هیچکس جلو نمیره. مثلاً فرض کن دو goroutine داریم که هر وقت میبینن منبعی اشغاله، سریع عقبنشینی میکنن و دوباره امتحان میکنن(استیتشون عوض میشه). اگر این عقبنشینیها و تلاشها بهشکلی باشه که هر بار دقیقاً با هم انجام بشن، هیچوقت به منبع دسترسی پیدا نمیکنن. برنامه گیر نمیکنه مثل deadlock، ولی همچنان هیچ پیشرفتی هم اتفاق نمیافته.
Starvation
گرسنگی زمانی پیش میاد که یه goroutine هیچوقت به منبعی که لازم داره دسترسی پیدا نمیکنه. مثلا اگه منابع همیشه توسط بقیه goroutineها اشغال باشن، اون یکی بینوا :) هیچوقت نوبتش نمیرسه. نه به خاطر اینکه قفل شده یا گیر کرده، بلکه چون اولویت یا سیاست اجرای برنامه اجازه نمیده بهش برسه.