چجوری Channel ها در Golang کار میکنن


در این پست میخوایم راجب اینکه channel ها چطور کار میکنن و نحوه ی ارسال/دریافت دیتا روی اون ها به چه شکل هست صحبت کنیم.

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

همزمانی نیاز به همگام سازی دارد.

و گو برای رسیدن به این هدف از الگویی به نام CSP استفاده میکنه ( Communicating Sequential Processes) . و فلسفه اصلی همزمانی در گو به این صورت هست که :

برای برقراری ارتباط ، حافظه رو به اشتراک نذارید بلکه حافظه رو با برقراری ارتباط به اشتراک بزارید.

گوروتین یک thread سبک هست که توسط Go runtime مدیریت میشه و میتونه وظایف را به طور همزمان و به طور بالقوه به صورت موازی نیز اجرا کنه.

خب بیاین برای شروع یک buffered channel بسازیم:

ch := make(chan int, 3)

کد بالا یک Buffered Channel با ظرفیت ۳ و از نوع int میسازه.

در عمق ماجرا تابع make یک شی از نوعhchan داخل heap میسازه و یک اشاره گر که به اون اشاره میکنه رو بر میگردونه . یه سری از فیلد های ساختار hchan تو تصویر زیر اومده:

  • فیلد buf : یک اشاره گر به یک ارایه هست که نقش صف را دارد (فقط برای buffered channel استفاده میشود).
  • فیلد sendx : ایندکس عنصر ارسالی در آرایه بافر
  • فیلد recvx : ایندکس عنصر دریافتی در آرایه بافر
  • فیلد lock : باعث میشه مطمئن بشیم که خواندن و نوشتن روی channel بصورت atomic انجام بشه

در ادامه میخوایم سناریو های زیر رو بررسی کنیم:

  • ارسال و دریافت بدون بلاک شدن
  • دریافت از یک channel خالی
  • ارسال روی channel ای که پر هست

ارسال و دریافت بدون بلاک شدن:

وقتی که channel خالی باشه ما میتونیم آیتم هارو در صف بدون اینکه بلاک بشیم وارد کنیم.

ارسال بدون بلاک شدن
ارسال بدون بلاک شدن

و همین طور زمانی که channel خالی نباشه ما میتونیم آیتم هارو از جلوی صف بدون اینکه بلاک بشیم دریافت کنیم.

دریافت بدون بلاک شدن
دریافت بدون بلاک شدن


گوروتین های منتظر:

دو فیلد دیگه هست که زمانی که گوروتین های منتظر داریم خیلی برای ما مهم هستن :

  • فیلد recvq : گوروتین های بلاک شده ای که سعی داشتن از channel دیتا بخونن رو نگهداری میکنه.
  • فیلد sendq : گوروتین های بلاک شده ای که سعی داشتن به channel دیتا بفرستند رو نگهداری میکنه.


درضمن recvq , sendq از جنس linked list هستن.

type hchan struct {
buf unsafe.Pointer
sendx uint
recvx uint
lock mutex

sendq waitq
recvq waitq
... // more fields
}

type waitq struct {
first *sudog
last *sudog
}

// pseudo goroutine
type sudog struct {
g *g
elem unsafe.Pointer
next *sudog
prev *sudog
...

c *hchan
}


دریافت از یک channel خالی:

زمانی که channel خالی هست عملیات خواندن از اون باعث بلاک شدن اون گوروتین میشه.

فراموش نکنید که همه ی گوروتین هایی که ار انتظار دریافت هستن داخل صف recvq قرار میگیرن


راستی حواستون باشه که os thread بلاک نمیشه و فقط گوروتین بلاک میشه.

خب حالا سوال پیش میاد که این گوروتین بلاک شده کی دوباره شروع به کار میکنه ؟؟؟

همون طور که حدس میزنین جوابش میشه وقتی که یه گوروتین دیگه چیزی روی این channel بفرسته!

بیاین باهم یه بررسی بکنیم ببینیم چه اتفاق هایی رخ میده:

  • گوروتین جدید دیتا ای رو که میخواد بفرسته رو مستقیم کپی میکنه داخل element اولین گوروتین ای که در صف منتظره !!!!!!!!!!!!!!!!!!
  • اولین گوروتین در حال انتظار از recvq برداشته و از صف حدف میشه.
  • حالا runtime scheduler گوروتین برداشته شده رو داخل runqueue میذاره . بنابرین الان گوروتین بلاک شده آمادست که دوباره اجرا بشه.

احتمالا خیلی شوکه شدین از اینکه وقتی بافر خالی باشه گوروتینی که میخواد دیتا رو بفرسته داخل channel مستقیم اون رو کپی می کنه داخل elem گوروتین ای که منتظر هست و اول دیتارو داخل بافر نمی فرسته

برای این انقدر عجیبه که گوروتین ها برای خودشون stack جدا دارن و معمولا هیچ وقت گوروتین ها از stack گوروتین دیگه ای به طور مستقیم نمیخونن و نمی نویسن . فقط در این حالته که گوروتین ها همچین کاری میکنن. و دلیلش هم بهبود پرفورمنس هست.


ارسال روی channel ای که پر هست

زمانی که channel پر هست عملیات ارسال به اون باعث بلاک شدن گوروتین میشه.

ارسال روی channel پر
ارسال روی channel پر

تا وقتی که یک گوروتین دیگه از channel چیزی دریافت کنه بقیه گوروتین ها بلاک شده باقی میمونن.

بیاین جزییاتش رو باهم بررسی کنیم:

  1. زمانی که یک گوروتین جدید عملیات خوندن از channel رو انجام میده , اولین عنصر موجود در بافر پاک میشه
  2. اولین گوروتین منتظر از صف sendq برداشته و حذف میشه
  3. مقدار element گوروتین برداشته شده داخل بافر کپی میشه
  4. حالا runtime scheduler گوروتین برداشته شده رو داخل runqueue میذاره . بنابرین الان گوروتین بلاک شده آمادست که دوباره اجرا بشه



امیدوارم که این پست تونسته باشه خلاصه ای از نحوی کار channel ها توی Golang رو شرح بده

از این که زمان گذاشتید و این پست رو خوندید خیلی ممنون . خوشحال میشم اگه این پست رو خوندید در کامنت ها نظرات خودتون رو مطرح کنید.


منابع: