آموزش کانکارنسی در گولنگ - کار با `sync.Cond` در Goroutine

در برنامه‌نویسی همزمان (Concurrent Programming)، همگام‌سازی (Synchronization) نقش کلیدی در جلوگیری از Race Conditions و اطمینان از عملکرد هماهنگ بین تردها یا گوروتین‌ها دارد. تصور کنید مسئله‌ای دارید که باید چند تولیدکننده (Producer) و مصرف‌کننده (Consumer) را که به یک منبع مشترک مثل یک بافر یا صف دسترسی دارند، هماهنگ کنید. این چالش کلاسیک در حوزه هم‌زمانی با نام "مسئله تولیدکننده-مصرف‌کننده" شناخته می‌شود.

در چنین شرایطی، همگام‌سازی ضروری است تا تولیدکننده‌ها داده‌ها را بازنویسی نکنند و مصرف‌کننده‌ها داده‌های نامعتبر یا قدیمی را نخوانند. این همگام‌سازی ضروری است، چون در صورت نبود آن، دسترسی هم‌زمان به داده‌های مشترک می‌تواند منجر به شرایط رقابتی، خرابی داده‌ها یا حتی کرش برنامه شود.

تولیدکننده‌ها باید منتظر بمانند اگر بافر پر باشد، و مصرف‌کننده‌ها نیز باید منتظر بمانند اگر بافر خالی باشد. گاهی ممکن است با بافری محدود و دارای اندازه ثابت مواجه شوید که باید دسترسی چند تولیدکننده و مصرف‌کننده به آن به‌درستی مدیریت شود.


sync.Cond چی هست؟

در زبان گو، ساختار sync.Cond یک مکانیزم سیگنال‌دهی است که به گوروتین‌ها اجازه می‌دهد تا زمانی منتظر بمانند که یک شرط خاص برقرار شود. این قابلیت به‌ویژه در هماهنگ‌سازی جریان‌های کاری پیچیده کاربرد دارد؛ جایی که برخی از گوروتین‌ها باید اجرای خود را متوقف کرده و منتظر بمانند تا گوروتین‌های دیگر عملیات مشخصی را انجام دهند.

مفاهیم اصلی پشت sync.Cond:

  • مسدودسازی (Blocking): گوروتین‌ها می‌توانند منتظر دریافت یک سیگنال بمانند و تا زمان دریافت آن، اجرای خود را متوقف کنند.

  • سیگنال‌دهی (Signaling): سایر گوروتین‌ها می‌توانند با ارسال سیگنال، گوروتین‌های منتظر را از برقرار شدن شرط مطلع کنند.

  • بهره‌وری (Efficiency): با به خواب بردن گوروتین‌ها تا زمان دریافت سیگنال، از مصرف بی‌مورد منابع (Busy Waiting) جلوگیری می‌شود.


نحوه کار sync.Cond:

  • مقدمه‌سازی (sync.Cond Initialization):
    برای استفاده از sync.Cond باید یک Locker تعریف کنید؛ معمولاً از sync.Mutex یا sync.RWMutex استفاده می‌شود. این Locker وظیفه محافظت از منابع اشتراکی را برعهده دارد.

  • تابع Wait():
    وقتی یک گوروتین Wait() را فراخوانی می‌کند:

    • ابتدا قفل مربوطه را آزاد می‌کند تا گوروتین‌های دیگر بتوانند به منبع مشترک دسترسی داشته باشند.

    • سپس منتظر می‌ماند (مسدود می‌شود) تا سیگنال دریافت کند.

    • پس از دریافت سیگنال، مجدداً قفل را به‌دست می‌آورد و به اجرای خود ادامه می‌دهد.

  • توابع Signal() و Broadcast():

    • Signal() فقط یک گوروتین منتظر را بیدار می‌کند تا ادامه دهد.

    • Broadcast() تمام گوروتین‌های منتظر را بیدار می‌کند.


مسئله: تولیدکننده–مصرف‌کننده با استفاده از Mutex و متغیر شرطی (Condition Variable)

تصور کنید یک بافر (یا صف) با اندازه‌ای ثابت دارید. چندین تولیدکننده وظیفه تولید آیتم و افزودن آن به بافر را دارند، در حالی‌ که چندین مصرف‌کننده این آیتم‌ها را از بافر حذف می‌کنند. چالش اصلی در این سناریو عبارت است از:

  • اطمینان حاصل شود که تولیدکننده‌ها فقط زمانی آیتم اضافه کنند که در بافر فضا وجود داشته باشد.

  • اطمینان حاصل شود که مصرف‌کننده‌ها فقط زمانی آیتم حذف کنند که بافر خالی نباشد.

  • به تولیدکننده‌ها و مصرف‌کننده‌ها سیگنال داده شود تا زمانی که مجاز به افزودن یا حذف آیتم هستند، عملیات خود را انجام دهند.

در اینجا ساختار اولیه کد آورده شده است:

package main

import (
    "fmt"
    "sync"
    "time"
)

const bufferSize = 5

type Buffer struct {
    data []int
    mu   sync.Mutex
    cond *sync.Cond
}

func (b *Buffer) produce(item int) {
    // Producer logic to add item to the buffer
}

func (b *Buffer) consume() int {
    // Consumer logic to remove item from the buffer
    return 0
}

func main() {
    buffer := &Buffer{data: make([]int, 0, bufferSize)}
    buffer.cond = sync.NewCond(&buffer.mu)
    var wg sync.WaitGroup
    // Start producer goroutines
    for i := 1; i <= 3; i++ {
       wg.Add(1)
       go func(id int) {
          defer wg.Done()
          for j := 0; j < 5; j++ { // Each producer creates 5 items
             buffer.produce(id*10 + j) // Produce unique items based on id and j
             time.Sleep(100 * time.Millisecond)
          }
       }(i)
    }
    // Start consumer goroutines
    for i := 1; i <= 3; i++ {
       wg.Add(1)
       go func(id int) {
          defer wg.Done()
          for j := 0; j < 5; j++ { // Each consumer consumes 5 items
             item := buffer.consume()
             fmt.Printf("Consumer %d consumed item %d
", id, item)
             time.Sleep(150 * time.Millisecond)
          }
       }(i)
    }
    wg.Wait()
    fmt.Println("All producers and consumers finished.")
}

وظیفه‌ی ما به‌عنوان یک مهندس این است که متدهای produce و consume را پیاده‌سازی کنیم تا این الزامات برآورده شوند.

متد produce آیتم‌هایی را به بافر اضافه می‌کند و زمانی که آیتمی اضافه شد، مصرف‌کننده‌ها را مطلع می‌سازد.
متد consume آیتم‌هایی را از بافر حذف می‌کند و زمانی که آیتمی حذف شد، تولیدکننده‌ها را مطلع می‌سازد.

این مسئله را می‌توان به‌راحتی با استفاده از sync.Cond حل کرد، به‌طوری که گوروتین‌ها هنگام پر بودن یا خالی بودن بافر منتظر بمانند و در زمان مناسب سیگنال دریافت کنند.


استفاده از sync.Cond در مثال بالا

در اینجا، sync.NewCond(&buffer.mu) یک متغیر شرطی (condition variable) جدید ایجاد می‌کند که با mutex به نام mu مرتبط است. این متغیر شرطی امکان منتظر ماندن (waiting) و سیگنال‌دهی (signaling) را در زمان تغییرات بافر (مانند اضافه یا حذف آیتم‌ها) فراهم می‌کند:

buffer.cond = sync.NewCond(&buffer.mu)


Producer Method:

func (b *Buffer) produce(item int) {
    b.mu.Lock()
    defer b.mu.Unlock()

    // Wait if the buffer is full
    for len(b.data) == bufferSize {
       b.cond.Wait() // Release lock and wait until signaled
    }

    // Add item to the buffer
    b.data = append(b.data, item)
    fmt.Printf("Produced item %d
", item)

    // Signal a consumer that an item is available
    b.cond.Signal()
}

قفل کردن (Lock):
تولیدکننده mu را قفل می‌کند تا اطمینان حاصل کند که دسترسی انحصاری به b.data دارد.

انتظار در صورت پر بودن بافر (Wait if Full):
اگر بافر پر باشد، تولیدکننده متد b.cond.Wait() را فراخوانی می‌کند:
این کار باعث می‌شود قفل b.mu آزاد شود تا یک مصرف‌کننده بتواند آیتمی از بافر حذف کند.
سپس تولیدکننده تا زمانی که مصرف‌کننده سیگنال دهد (و فضایی در بافر ایجاد شود)، منتظر می‌ماند (مسدود می‌شود).

افزودن آیتم و سیگنال‌دهی (Add Item and Signal):
وقتی فضایی در بافر ایجاد شد، تولیدکننده:

  • آیتم را به بافر اضافه می‌کند.

  • متد b.cond.Signal() را فراخوانی می‌کند تا یک مصرف‌کننده منتظر (در صورت وجود) را مطلع سازد که اکنون آیتمی برای مصرف وجود دارد.


Consumer Method:

func (b *Buffer) consume() int {
    b.mu.Lock()
    defer b.mu.Unlock()

    // Wait if the buffer is empty
    for len(b.data) == 0 {
       b.cond.Wait() // Release lock and wait until signaled
    }

    // Remove item from the buffer
    item := b.data[0]
    b.data = b.data[1:]
    fmt.Printf("Consumed item %d
", item)

    // Signal a producer that space is available
    b.cond.Signal()

    return item
}

قفل کردن (Lock):
مصرف‌کننده mu را قفل می‌کند تا دسترسی انحصاری به b.data داشته باشد.

انتظار در صورت خالی بودن بافر (Wait if Empty):
اگر بافر خالی باشد، مصرف‌کننده متد b.cond.Wait() را فراخوانی می‌کند:
این کار باعث می‌شود قفل b.mu آزاد شود تا یک تولیدکننده بتواند آیتمی تولید کند و پس از آماده شدن، سیگنال دهد.
مصرف‌کننده تا زمانی که آیتمی برای مصرف وجود داشته باشد، منتظر می‌ماند.

مصرف آیتم و سیگنال‌دهی (Consume Item and Signal):
وقتی آیتمی در بافر موجود شد، مصرف‌کننده:

  • آن را حذف می‌کند.

  • متد b.cond.Signal() را فراخوانی می‌کند تا یک تولیدکننده منتظر را مطلع کند که اکنون فضایی در بافر برای تولید آیتم وجود دارد.


چرا sync.Cond در اینجا مؤثر است؟

  • متغیر شرطی (Condition Variable):
    sync.Cond روشی کارآمد برای مدیریت وضعیت‌هایی فراهم می‌کند که در آن‌ها بافر یا پر است یا خالی، بدون نیاز به بررسی مداوم (loop) غیرضروری.

  • مکانیزم انتظار و سیگنال‌دهی (Wait and Signal Mechanism):
    متد Wait() به‌صورت خودکار قفل را آزاد می‌کند، که این کار از بروز deadlock جلوگیری کرده و اجازه می‌دهد گوروتین‌های دیگر در زمان مناسب اجرا شوند.

  • هماهنگی (Coordination):
    با استفاده از Signal()، عملیات تولید و مصرف را هماهنگ می‌کنیم و اطمینان حاصل می‌شود که هر گوروتین فقط زمانی منتظر بماند که واقعاً نیاز باشد. این کار از دستکاری بافر پر یا خالی جلوگیری می‌کند.

این هماهنگی به تولیدکننده‌ها و مصرف‌کننده‌ها اجازه می‌دهد که بافر را بدون تداخل به‌صورت اشتراکی استفاده کنند و دسترسی به آن را براساس وضعیت بافر به‌شکل مؤثر مدیریت نمایند.

  • تولیدکننده‌ها در صورت پر بودن بافر منتظر می‌مانند و پس از تولید یک آیتم، به مصرف‌کننده‌ها سیگنال می‌دهند.

  • مصرف‌کننده‌ها در صورت خالی بودن بافر منتظر می‌مانند و پس از مصرف یک آیتم، به تولیدکننده‌ها سیگنال می‌دهند.


سناریوهای دیگر برای استفاده از sync.Cond

تصور کنید وظایفی دارید که در آن‌ها چندین گوروتین باید منتظر بمانند تا یک شرط خاص برقرار شود، سپس ادامه دهند. نمونه‌هایی از این موارد عبارتند از:

  • پردازش دسته‌ای (Batch Processing):
    منتظر ماندن تا تعداد مشخصی از وظایف جمع شوند، سپس همه آن‌ها را به‌صورت یکجا پردازش کردن.

  • هماهنگی رویدادها (Event Coordination):
    منتظر ماندن برای وقوع یک رویداد خاص (مثلاً بارگذاری شدن داده‌ها یا در دسترس قرار گرفتن یک منبع).

  • محدودسازی نرخ (Rate Limiting):
    کنترل تعداد عملیات هم‌زمان برای جلوگیری از استفاده بیش‌ازحد از منابع.

در این سناریوها، sync.Cond روشی کارآمد برای مدیریت همگام‌سازی گوروتین‌ها بر اساس شرایط خاص فراهم می‌کند، و آن را به گزینه‌ای مناسب برای مسائلی تبدیل می‌کند که نیاز به هماهنگی بین وظایف هم‌زمان دارند.

0 🔥
0 🎉
0 😮
0 👍
0 💜
0 👏
میلاد خسروی
نویسنده کد نیوز

برنامه نویس فان | Fun Developer یک آدم ساده که عاشق برنامه نویسی و کد زدنه :) تلاش میکنه تا به بقیه کمک کنه. توسعه دهنده هسته لاراول و فضای اوپن سورس. فاندر پرانتز و کد نیوز.

0+ نظر

برای ثبت نظر ابتدا ورود کنید.

0 نظر

    اولین نفر باش که نظر ثبت میکنی :) یعنی یه کامنت به ما نمیرسه 😁