3.3 پکیج sync

3.3 پکیج sync

پکیج sync توی گو مثل یک جعبه‌ابزار تخصصی برای همگام‌سازی (synchronize) و مدیریت دسترسی چندتا goroutine به داده یا منبع مشترکه.
وقتی چند goroutine همزمان به یک داده دسترسی پیدا می‌کنن، بدون هماهنگی ممکنه داده خراب بشه یا رفتار برنامه غیرقابل پیش‌بینی بشه (Data Race).

sync دقیقا برای جلوگیری از این اتفاق ساخته شده و ابزارهایی رو ارائه می‌ده که اجازه می‌ده دسترسی همزمان رو کنترل کنید.

ابزارهای اصلی sync:

  • Mutex — قفل ساده برای جلوگیری از دسترسی همزمان به منبع مشترک.
  • RWMutex — قفل خواندن/نوشتن: چند خواننده همزمان یا یک نویسنده در هر لحظه.
  • WaitGroup — منتظر موندن تا همه goroutineها کارشون رو تموم کنن.
  • Once — اجرای یک کد فقط یک بار حتی بین چند goroutine.
  • Pool — نگه‌داری مجموعه‌ای از آبجکت‌های آماده برای استفاده مجدد.
  • Cond — هماهنگی بر اساس شرط یا رویداد (Condition Variable).

نکته: sync فقط برای هماهنگ‌کردن دسترسی به منابع مشترک ساخته شده، نه برای زمان‌بندی یا اجرای همزمان.

توجه کنید که پکیج sync فقط و فقط برای مدیریت و همگام سازی دسترسی های گوروتین ها به یک داده مشترک استفاده می شود.

3.3.1 Mutex — قفل متقابل #

Mutex یا قفل متقابل یکی از اصلی‌ترین ابزارهای همگام‌سازی در زبان Go است که برای جلوگیری از دسترسی همزمان ناامن (Data Race) به داده‌های مشترک استفاده می‌شود. وقتی یک goroutine قفل را با Lock() می‌گیرد، سایر goroutineهایی که سعی کنند همان قفل را بگیرند، باید منتظر بمانند تا قفل با Unlock() آزاد شود. این مکانیزم تضمین می‌کند که در هر لحظه فقط یک goroutine می‌تواند بخش بحرانی (Critical Section) کد را اجرا کند. بخش بحرانی همان قسمتی از کد است که به داده‌ی مشترک دسترسی دارد و تغییر آن ممکن است باعث بروز خطا یا رفتار غیرقابل پیش‌بینی شود.

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

مثال تئوری — آشپزخانه و سیب‌زمینی سرخ‌کرده #

فرض کنید مادرتان در آشپزخانه در حال سرخ کردن سیب‌زمینی خلالی است. برای اینکه کسی وسط کار مزاحم نشود و نظم کار به هم نخورد، درِ آشپزخانه را می‌بندد. حالا این در مثل یک Mutex عمل می‌کند:

  • وقتی در بسته است (Lock شده)، فقط مادرتان داخل آشپزخانه است و می‌تواند روی سیب‌زمینی‌ها کار کند.
  • اگر کسی (مثلاً یکی از بچه‌ها) بخواهد وارد آشپزخانه شود، باید منتظر بماند تا مادرتان در را باز کند (Unlock).
  • وقتی مادرتان کارش را تمام کرد و در را باز کرد، نفر بعدی می‌تواند وارد شود و کاری انجام دهد.

حالا فرض کنید در طول کار، یکی از بچه‌ها خیلی عجله دارد و بدون اجازه وارد می‌شود و یک مشت سیب‌زمینی برمی‌دارد. این همان Data Race است که بدون قفل اتفاق می‌افتد و می‌تواند باعث خراب شدن برنامه (یا در این مثال، کمتر شدن سیب‌زمینی‌ها 😄) شود. Mutex دقیقاً برای جلوگیری از این نوع تداخل ساخته شده است.

sync mutex

3.1.1.1 ساختار Mutex در Go #

sync.Mutex دو متد اصلی داره:

  1. Lock()
    • وقتی یک goroutine این متد رو صدا بزنه، قفل رو می‌گیره.
    • اگه قفل قبلاً توسط goroutine دیگه گرفته شده باشه، این goroutine منتظر می‌مونه (مسدود می‌شه) تا قفل آزاد بشه.
  2. Unlock()
    • قفل رو آزاد می‌کنه تا goroutineهای دیگه بتونن وارد بخش قفل‌شده بشن.

نکته مهم تولیدی:
بعد از هر Lock() بلافاصله defer Unlock() بنویسید تا حتی اگر وسط کار panic یا return اتفاق افتاد، قفل آزاد بشه:

mu.Lock()
defer mu.Unlock()

3.1.1.2 مثال ساده Mutex #

در این مثال، چهار goroutine می‌خوان همزمان مقدار یک متغیر مشترک (count) رو تغییر بدن:

package main

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

var count int // متغیر مشترک
var mu sync.Mutex

func main() {
	for i := 0; i < 4; i++ {
		go increment()
	}
	time.Sleep(time.Second)
}

func increment() {
	mu.Lock()           // گرفتن قفل
	defer mu.Unlock()   // آزاد کردن قفل بعد از اتمام کار
	count++
	fmt.Printf("Count: %d\n", count)
}

3.1.1.3 نکات کاربردی و تولیدی Mutex #

  1. محدوده قفل رو کوچک نگه دارید
    قفل رو فقط برای بخشی از کد که نیاز به حفاظت داره بگیرید. قفل‌کردن بخش‌های بزرگ کد می‌تونه باعث افت کارایی بشه.
  2. از کپی Mutex خودداری کنید
    همیشه Mutex رو با اشاره‌گر (pointer) پاس بدید، چون کپی‌کردن Mutex می‌تونه باعث رفتار غیرقابل پیش‌بینی بشه.
  3. احتیاط در قفل‌های تو در تو (Nested Locks)
    گرفتن چند قفل به ترتیب اشتباه می‌تونه باعث Deadlock بشه. همیشه ترتیب قفل‌گیری رو یکسان نگه دارید.
  4. استفاده در ساختار داده‌ها
    می‌تونید Mutex رو به عنوان فیلد در یک struct بذارید تا عملیات روی داده‌های اون struct ایمن بشه.

3.1.1.4 مثال پیشرفته: Mutex در struct #

package main

import (
	"fmt"
	"sync"
)

type SafeCounter struct {
	mu    sync.Mutex
	value int
}

func (c *SafeCounter) Inc() {
	c.mu.Lock()
	defer c.mu.Unlock()
	c.value++
}

func (c *SafeCounter) Value() int {
	c.mu.Lock()
	defer c.mu.Unlock()
	return c.value
}

func main() {
	counter := &SafeCounter{}
	var wg sync.WaitGroup

	for i := 0; i < 1000; i++ {
		wg.Add(1)
		go func() {
			defer wg.Done()
			counter.Inc()
		}()
	}

	wg.Wait()
	fmt.Println("Final Count:", counter.Value())
}

3.1.1.5 مزایا و معایب Mutex #

مزایا:

  • پیاده‌سازی ساده و مستقیم.
  • بدون overhead اضافی برای مدیریت channel یا ساختار پیچیده.

معایب:

  • قفل‌گذاری بیش از حد می‌تونه باعث افت کارایی بشه.
  • اشتباه در مدیریت Lock/Unlock ممکنه باعث Deadlock یا Data Race بشه.

3.3.1.6 سناریوهای استفاده #

  1. همگام سازی دسترسی به متغیرهای مشترک: یک mutex می تواند برای همگام سازی دسترسی به متغیرهای مشترک بین چندین گوروتین استفاده شود. این می تواند در مواردی مفید باشد که چندین گوروتین نیاز به خواندن یا به روز رسانی یک متغیر به طور همزمان دارند.

  2. هماهنگی دسترسی به حالت مشترک: یک mutex می تواند برای هماهنگ کردن دسترسی به حالت مشترک بین چندین گوروتین استفاده شود. به عنوان مثال، ممکن است از یک mutex استفاده کنید تا اطمینان حاصل کنید که فقط یک گوروتین می تواند یک ساختار داده مشترک را در یک زمان تغییر دهد.

  3. پیاده سازی الگوهای تولیدکننده-مصرف کننده (producer-consumer): یک mutex می تواند برای پیاده سازی الگوهای تولیدکننده-مصرف کننده استفاده شود، که در آن یک یا چند گوروتین داده تولید می کنند و یک یا چند گوروتین آن را مصرف می کنند. mutex می تواند برای همگام سازی دسترسی به ساختار داده مشترک که داده ها را نگه می دارد استفاده شود.

۲ نکته خیلی مهم

  1. سعی کنید پس از اینکه تابع Lock را فراخوانی میکنید تابع Unlock را داخل defer قرار دهید.
  2. زمانیکه قصد دارید Mutex را به عنوان پارامتر ورودی برای توابع تعریف کنید بهتر است از نوع اشاره گر باشد.

3.3.2 RWMutex — قفل خواندن/نوشتن #

RWMutex مخفف Read-Write Mutex هست و درواقع نسخه‌ی پیشرفته‌تر Mutex محسوب می‌شه.
وظیفه‌اش همگام‌سازی دسترسی همزمان به یک داده یا منبع مشترکه، ولی با یک قابلیت اضافه: چند goroutine می‌تونن همزمان داده رو بخونن، ولی فقط یک goroutine می‌تونه بنویسه و وقتی در حال نوشتنه، هیچ‌کس حق خواندن نداره.

3.3.2.1 چرا RWMutex؟ #

در خیلی از برنامه‌ها، تعداد عملیات خواندن (Read) روی داده‌ها خیلی بیشتر از نوشتن (Write) هست. مثلا:

  • کش (Cache) که بیشتر وقت‌ها داده رو از حافظه می‌خونیم و فقط گاهی به‌روزرسانی می‌کنیم.
  • تنظیمات برنامه (Configuration) که بارها خونده می‌شه ولی به ندرت تغییر پیدا می‌کنه.

در این مواقع، استفاده از یک Mutex ساده باعث می‌شه حتی وقتی چند goroutine فقط می‌خوان داده رو بخونن، مجبور بشن منتظر هم بمونن. اما RWMutex اجازه می‌ده چند خواننده همزمان کار کنن و فقط موقع نوشتن همه منتظر بمونن.

3.3.2.2 متدهای اصلی RWMutex #

  • RLock() — گرفتن قفل خواندن. چند goroutine می‌تونن همزمان RLock بگیرن.
  • RUnlock() — آزاد کردن قفل خواندن.
  • Lock() — گرفتن قفل نوشتن. فقط یک goroutine می‌تونه همزمان Lock بگیره و تا زمانی که قفل نوشتن فعاله، هیچ‌کس نمی‌تونه بخونه یا بنویسه.
  • Unlock() — آزاد کردن قفل نوشتن.

3.3.2.3 مثال ساده RWMutex #

package main

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

type SafeMap struct {
	mu   sync.RWMutex
	data map[string]string
}

func (s *SafeMap) Read(key string) string {
	s.mu.RLock()
	defer s.mu.RUnlock()
	return s.data[key]
}

func (s *SafeMap) Write(key, value string) {
	s.mu.Lock()
	defer s.mu.Unlock()
	s.data[key] = value
}

func main() {
	smap := SafeMap{data: make(map[string]string)}

	// نویسنده
	go func() {
		for i := 0; i < 5; i++ {
			smap.Write("name", fmt.Sprintf("value-%d", i))
			time.Sleep(500 * time.Millisecond)
		}
	}()

	// چند خواننده همزمان
	for i := 0; i < 3; i++ {
		go func(id int) {
			for j := 0; j < 5; j++ {
				fmt.Printf("Reader %d: %s\n", id, smap.Read("name"))
				time.Sleep(300 * time.Millisecond)
			}
		}(i)
	}

	time.Sleep(3 * time.Second)
}

در این مثال، چند goroutine همزمان از متد Read() استفاده می‌کنن و چون RLock() استفاده شده، همه می‌تونن بدون منتظر موندن بخونن. ولی موقع نوشتن، قفل نوشتن (Lock()) همه رو متوقف می‌کنه تا عملیات امن انجام بشه.

3.3.2.4 نکات و ترفندهای استفاده از RWMutex #

  1. وقتی بیشتر خواندن داریم، از RWMutex استفاده کنید
    اگر نسبت خواندن به نوشتن پایین باشه (یعنی نوشتن زیاد باشه)، استفاده از RWMutex ممکنه حتی کندتر از Mutex ساده باشه، چون مدیریت قفل‌های خواندن/نوشتن پیچیده‌تره.
  2. هرگز قفل خواندن و نوشتن را همزمان نگیرید
    گرفتن RLock() و سپس تلاش برای گرفتن Lock() در همان goroutine باعث Deadlock می‌شه.
  3. محدوده قفل را کوچک نگه دارید
    فقط همان بخش حساس به تغییر داده را قفل کنید. این کار باعث افزایش عملکرد و کاهش زمان انتظار می‌شود.
  4. قفل‌ها را جفت باز و بسته کنید
    هر RLock() باید با RUnlock() و هر Lock() باید با Unlock() جفت شود.

3.3.2.5 مثال کاربردی — کش خواندنی/نوشتنی #

فرض کنید یک سیستم داریم که قیمت ارز را ذخیره می‌کند:

  • ده‌ها goroutine در حال خواندن قیمت هستند.
  • فقط یک goroutine هر چند ثانیه یکبار قیمت را به‌روزرسانی می‌کند.

در این سناریو RWMutex باعث می‌شود خواننده‌ها معطل همدیگر نشوند و فقط هنگام بروزرسانی، همه منتظر بمانند.

type PriceCache struct {
	mu    sync.RWMutex
	price float64
}

func (p *PriceCache) Get() float64 {
	p.mu.RLock()
	defer p.mu.RUnlock()
	return p.price
}

func (p *PriceCache) Set(val float64) {
	p.mu.Lock()
	defer p.mu.Unlock()
	p.price = val
}

3.3.3 WaitGroup — هماهنگی پایان کار goroutineها #

WaitGroup در پکیج sync یکی از پرکاربردترین ابزارها برای همگام‌سازی است که به شما اجازه می‌دهد منتظر بمانید تا گروهی از goroutineها کارشان را تمام کنند.
ایده‌اش ساده است: شما قبل از اجرای goroutineها به WaitGroup می‌گویید که چندتا کار قرار است انجام شود، هر goroutine بعد از اتمام کار به WaitGroup خبر می‌دهد، و وقتی همه کارها تمام شد، برنامه ادامه پیدا می‌کند.

3.3.3.1 متدهای اصلی WaitGroup #

  1. Add(delta int)
    • تعداد کارهایی که WaitGroup باید منتظرشان بماند را اضافه یا کم می‌کند.
    • معمولاً قبل از اجرای goroutineها استفاده می‌شود (Add(n) برای n تا goroutine).
  2. Done()
    • نشان می‌دهد که یکی از کارها تمام شده است.
    • معادل Add(-1) است.
    • معمولاً در ابتدای goroutine با defer فراخوانی می‌شود.
  3. Wait()
    • برنامه را تا زمانی که شمارش WaitGroup به صفر برسد مسدود می‌کند.

3.3.3.2 مثال ساده WaitGroup #

package main

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

func main() {
	var wg sync.WaitGroup
	wg.Add(3) // انتظار برای 3 goroutine

	go worker("A", &wg)
	go worker("B", &wg)
	go worker("C", &wg)

	wg.Wait() // صبر تا پایان همه goroutineها
	fmt.Println("All workers finished!")
}

func worker(name string, wg *sync.WaitGroup) {
	defer wg.Done() // اعلام پایان کار
	fmt.Printf("Worker %s starting...\n", name)
	time.Sleep(time.Second)
	fmt.Printf("Worker %s done.\n", name)
}

3.3.3.3 مثال کاربردی — دانلود همزمان فایل‌ها #

فرض کنید می‌خواهیم چند فایل را به صورت موازی دانلود کنیم و منتظر بمانیم تا همه دانلودها کامل شوند:

func downloadFile(url string, wg *sync.WaitGroup) {
	defer wg.Done()
	fmt.Println("Downloading:", url)
	time.Sleep(2 * time.Second)
	fmt.Println("Downloaded:", url)
}

func main() {
	var wg sync.WaitGroup
	files := []string{"file1.zip", "file2.zip", "file3.zip"}

	wg.Add(len(files))
	for _, f := range files {
		go downloadFile(f, &wg)
	}

	wg.Wait()
	fmt.Println("All downloads complete!")
}

3.3.3.4 نکات و ترفندهای استفاده از WaitGroup #

  1. همیشه Add() را قبل از شروع goroutineها انجام دهید
    اگر Add() را بعد از شروع goroutineها انجام دهید، ممکن است WaitGroup صفر شود و Wait() بلافاصله ادامه پیدا کند (Race Condition).
  2. از اشاره‌گر برای پاس دادن WaitGroup استفاده کنید
    WaitGroup را کپی نکنید، چون هر کپی شمارش خودش را دارد و هماهنگی از بین می‌رود.
  3. تعداد Add و Done باید دقیقاً برابر باشد
    اگر تعداد Done() کمتر از Add() باشد، Wait() برای همیشه مسدود می‌ماند.
    اگر تعداد بیشتر باشد، Panic رخ می‌دهد.
  4. ترکیب با Channel برای نتایج
    می‌توانید WaitGroup را با Channel ترکیب کنید تا بعد از اتمام همه goroutineها، داده‌ها را پردازش کنید.

3.3.3.5 اشتباه رایج — Add داخل goroutine #

اشتباه:

for _, task := range tasks {
	go func(t string) {
		wg.Add(1) // ❌ اشتباه
		doTask(t)
		wg.Done()
	}(task)
}

چرا اشتباهه؟

چون ممکنه goroutine هنوز Add نکرده باشه ولی main Wait() رو صدا بزنه و شمارش صفر بشه.

راه درست:

wg.Add(len(tasks))
for _, task := range tasks {
	go func(t string) {
		defer wg.Done()
		doTask(t)
	}(task)
}

3.3.3.6 مثال پیشرفته — پردازش موازی با WaitGroup و Semaphore #

گاهی تعداد goroutineهای همزمان باید محدود شود. ترکیب WaitGroup با Channel به عنوان Semaphore می‌تواند این کار را انجام دهد:

اگر درخصوص پترن Semaphore بیشتر میخواید بدانید به اینجا مراجعه کنید.
func process(id int, wg *sync.WaitGroup, sem chan struct{}) {
	defer wg.Done()
	sem <- struct{}{}         // گرفتن اسلات
	fmt.Printf("Processing %d\n", id)
	time.Sleep(time.Second)
	<-sem                      // آزاد کردن اسلات
}

func main() {
	var wg sync.WaitGroup
	sem := make(chan struct{}, 3) // حداکثر 3 goroutine همزمان

	for i := 0; i < 10; i++ {
		wg.Add(1)
		go process(i, &wg, sem)
	}

	wg.Wait()
	fmt.Println("All processing complete!")
}

۳ نکته خیلی مهم

  1. ساختار WaitGroup یکی از پرکاربرد ترین قابلیت ها برای بحث همزمانی می باشد و برخی اوقات برای جلوگیری از Data race و همچنین برای ترتیب دهی عملیات همزمانی هم می توان از این استفاده کرد.
  2. سعی کنید WaitGroup را بصورت اشاره گر به توابعی که داخل گوروتین قرار دارند پاس دهید.
  3. هیچوقت تعداد گوروتین را بیشتر یا کمتر از اون تعدادی که دارید به متد Add ‌ندهید چون با خطای Panic مواجه خواهید شد.

3.3.4 Once — اجرای یک کد فقط یکبار #

sync.Once یکی از ابزارهای پکیج sync هست که تضمین می‌کنه یک تکه کد فقط یکبار در کل طول اجرای برنامه اجرا بشه، حتی اگر چندین goroutine به طور همزمان اون رو صدا بزنن.

این ابزار معمولاً برای کارهایی مثل مقداردهی اولیه (Initialization) یا ساخت منابع مشترک استفاده می‌شه که نیازه فقط یکبار انجام بشن.

3.3.4.1 متد اصلی Once #

  • Do(func())
    این متد یک تابع رو می‌گیره و فقط اولین باری که صدا زده بشه، اون تابع رو اجرا می‌کنه.
    اگر چند goroutine همزمان Do() رو صدا بزنن، فقط یکی اجرا می‌شه و بقیه منتظر می‌مونن تا تابع اجرا بشه.

3.3.4.2 چرا از Once استفاده کنیم؟ #

بدون Once، اگر بخواهیم کاری رو فقط یکبار انجام بدیم، مجبوریم از یک Mutex برای محافظت از کد استفاده کنیم و خودمون وضعیت (state) رو چک کنیم که آیا قبلاً اجرا شده یا نه. Once این کار رو به صورت thread-safe و بهینه انجام می‌ده، بدون اینکه نیاز باشه ما خودمون مدیریت کنیم.

3.3.4.3 مثال ساده Once #

package main

import (
	"fmt"
	"sync"
)

var once sync.Once

func initConfig() {
	fmt.Println("Config initialized")
}

func main() {
	for i := 0; i < 5; i++ {
		go once.Do(initConfig)
	}

	// کمی صبر می‌کنیم تا goroutineها اجرا بشن
	var wg sync.WaitGroup
	wg.Add(1)
	go func() {
		defer wg.Done()
	}()
	wg.Wait()
}

حتی با اجرای چندین goroutine، پیام فقط یکبار چاپ می‌شه.

3.3.4.4 استفاده از Once برای پیاده‌سازی Singleton #

Once یکی از بهترین راه‌ها برای پیاده‌سازی الگوی Singleton در Go هست، چون به صورت ایمن و همزمانی (thread-safe) تضمین می‌کنه که فقط یک instance ساخته می‌شه.

type singleton struct {
	data string
}

var (
	instance *singleton
	once     sync.Once
)

func GetInstance() *singleton {
	once.Do(func() {
		instance = &singleton{data: "my singleton data"}
	})
	return instance
}

func main() {
	s1 := GetInstance()
	s2 := GetInstance()
	if s1 == s2 {
		fmt.Println("Same instance")
	}
}

مزیت: نیازی به نوشتن قفل‌های اضافه یا متغیر flag نیست، چون Once همه چیز رو مدیریت می‌کنه.

3.3.4.5 نکات و ترفندهای استفاده از Once #

  1. حتماً از همان متغیر Once برای تمام دسترسی‌ها استفاده کنید
    اگر در جاهای مختلف متغیرهای Once جداگانه بسازید، هرکدوم تابع رو یکبار اجرا می‌کنن.
  2. تابع Do نباید nil باشه
    اگر nil بدید، Panic اتفاق می‌افته.
  3. Once برای reset کردن نیست
    وقتی تابعی با Once اجرا شد، دیگه نمی‌تونید اون رو دوباره اجرا کنید. اگر نیاز به reset دارید، باید ساختار جدیدی بسازید.
  4. بهینه و سبک
    Once به صورت داخلی فقط در اولین اجرا قفل می‌گیره و بعد از اون بدون هزینه قفل، مستقیماً ادامه می‌ده.

3.3.5 Pool #

در نسخه ۱.۳ زبان گو امکانی تایپی به نام Pool در پکیج sync که امکان ایجاد استخر آبجکت ها بطور موقت بدون اینکه بخواهد بخشی از حافظه را اشتغال کند اضافه شد. هر آبجکتی که در Pool ذخیره شود بطور خودکار در هرزمانی بدون اینکه اطلاع رسانی کند حذف می شود. امکان استفاده مجدد از آبجکت هایی که داخل استخر می گیرند وجود دارد و این باعث می شود سربار استفاده حافظه کاهش یابد.

sync pool
package main

import (
	"bytes"
	"io"
	"os"
	"sync"
	"time"
)

var bufPool = sync.Pool{
	New: func() any {
		// The Pool's New function should generally only return pointer
		// types, since a pointer can be put into the return interface
		// value without an allocation:
		return new(bytes.Buffer)
	},
}

// timeNow is a fake version of time.Now for tests.
func timeNow() time.Time {
	return time.Unix(1136214245, 0)
}

func Log(w io.Writer, key, val string) {
	b := bufPool.Get().(*bytes.Buffer)
	b.Reset()
	// Replace this with time.Now() in a real logger.
	b.WriteString(timeNow().UTC().Format(time.RFC3339))
	b.WriteByte(' ')
	b.WriteString(key)
	b.WriteByte('=')
	b.WriteString(val)
	w.Write(b.Bytes())
	bufPool.Put(b)
}

func main() {
	Log(os.Stdout, "path", "/search?q=flowers")
}

در بالا ما یک متغیر به نام bufPool ایجاد کردیم که یک آبجکت از نوع bytes.Buffer ایجاد می کند. سپس داخل تابع Log ما با استفاده از متد Get آبجکت مورد نظر را از Pool خارج و داخل متغیر b قرار دادیم و پس از آن عملیات لازم را برروی Buffer انجام و در نهایت با استفاده از متد Put آبجکت را به Pool اضافه کردیم. حالا اتفاقی که صورت گرفته ما عملیاتی که نیاز داشتیم را برروی آبجکت انجام دادیم بدون اینکه بخوایم بخشی از حافظه را درگیر کنیم.

3.3.5.1 بنچمارک در خصوص Pool #

package main

import (
   "sync"
   "testing")

type Person struct {
   Age int
}

var personPool = sync.Pool{
   New: func() interface{} { return new(Person) },
}

func BenchmarkWithoutPool(b *testing.B) {
   var p *Person
   b.ReportAllocs()
   b.ResetTimer()
   for i := 0; i < b.N; i++ {
      for j := 0; j < 10000; j++ {
         p = new(Person)
         p.Age = 23
      }
   }
}

func BenchmarkWithPool(b *testing.B) {
   var p *Person
   b.ReportAllocs()
   b.ResetTimer()
   for i := 0; i < b.N; i++ {
      for j := 0; j < 10000; j++ {
         p = personPool.Get().(*Person)
         p.Age = 23
         personPool.Put(p)
      }
   }
}

3.3.5.2 مثال های کاربردی #

مثال اول :

فرض کنید شما می خواهید یک فایل csv را با کلی رکورد parse کنید. هر رکورد نیازمند این است که داخل یک ساختاری قرار بگیرد و ایجاد یک ساختار باعث می شود بخشی از حافظه اختصاص یابد به آن ساختار. حالا فکر کنید میلیون ها رکورد داشته باشید و این تخصیص حافظه می تواند باعث توقف برنامه شود.

حالا برای اینکه بخواهیم جلوی این اتفاق را بگیریم و سربار را کاهش دهیم بهتر است ما از sync.Pool استفاده کنیم و ساختارها را داخل استخر قرار دهیم. و زمانیکه هرکدام از ساختارها مورد نیاز نباشد می توانند داخل استخر قرار گیرند و مجدد استفاده شوند. اینکار باعث می شود بطور خیلی قابل توجهی تعداد تخصیص حافظه کاهش یابد و عملکرد برنامه چند برابر شود.

مثال دوم :

موارد استفاده دیگر از sync.Pool برای ذخیره کردن آبجکت های پرکاربرد نظیر کانکشن دیتابیس یا شبکه و همچنین آبجکت هایی که قصد داریم برروی آن عملیات serialize و deserialize انجام دهیم.

3.3.6 Cond #

پکیج sync.Cond یکی از ابزارهای پیشرفته همزمانی در زبان Go است که امکان پیاده‌سازی الگوی “مانیتور” یا همان شرط انتظاری (Condition Variable) را فراهم می‌کند. Cond به شما اجازه می‌دهد تا مجموعه‌ای از goroutineها را تا زمانی که یک شرط یا رویداد خاص برقرار نشده، به صورت امن و کارآمد (بدون busy-waiting یا مصرف بی‌مورد CPU) منتظر نگه دارید. زمانی که شرط مورد نظر برقرار شد، می‌توانید یک یا همه goroutineهای منتظر را بیدار کنید تا کارشان را ادامه دهند. این تکنیک، در بسیاری از الگوهای معروف concurrency مانند producer-consumer، صف انتظار، صف پیام و کنترل منابع محدود کاربرد اساسی دارد.

نحوه کار Cond و نقش قفل (Mutex / RWMutex) #

برای ساخت یک شیء Cond باید یک قفل (معمولاً از نوع *sync.Mutex یا *sync.RWMutex) به آن بدهید. این قفل به Cond اجازه می‌دهد تا وضعیت مشترک (shared state) را در میان چند goroutine به طور thread-safe بررسی و کنترل کند. قفل، تضمین می‌کند که هیچ دو goroutineای همزمان نتوانند وضعیت را تغییر دهند یا به متدهای Cond دسترسی پیدا کنند، که این برای جلوگیری از race condition کاملاً حیاتی است.

lock := &sync.Mutex{}
cond := sync.NewCond(lock)

عملکرد متدها #

  • Wait(): وقتی یک goroutine متد Wait را صدا می‌زند، دو اتفاق پشت‌سرهم رخ می‌دهد:

    1. قفل داده‌شده (مثلاً Mutex) به طور موقت آزاد می‌شود تا سایر goroutineها بتوانند وضعیت مشترک را تغییر دهند.
    2. goroutine تا زمان دریافت سیگنال (Signal یا Broadcast) به حالت تعلیق (sleep) می‌رود و هیچ پردازشی انجام نمی‌دهد (کاملاً غیرمسدودکننده). پس از دریافت سیگنال و بیدار شدن، Wait دوباره به صورت اتمیک قفل را در اختیار می‌گیرد و اجرا از همان خط ادامه پیدا می‌کند. معمولاً قبل از Wait باید شرط را داخل یک حلقه (for) بررسی کنید تا از spurious wakeup و رقابت داده‌ای جلوگیری شود:
    cond.L.Lock()
    for !شرط_برقرار_است {
        cond.Wait()
    }
    // ادامه منطق ...
    cond.L.Unlock()
    
  • Signal(): این متد تنها یکی از goroutineهای منتظر را بیدار می‌کند (اگر کسی در صف انتظار باشد). انتخاب اینکه کدام goroutine بیدار شود به سیاست زمان‌بندی runtime وابسته است و تضمینی برای ترتیب خاصی وجود ندارد. Signal معمولاً زمانی به کار می‌رود که انتظار دارید فقط یک مصرف‌کننده با داده جدید یا تغییر وضعیت بیدار شود.

  • Broadcast(): این متد همه goroutineهای منتظر روی آن Cond را بیدار می‌کند تا شرط را دوباره بررسی کنند. Broadcast زمانی کاربرد دارد که یک رویداد می‌تواند برای همه‌ی منتظرها مهم باشد (مثلاً اتمام کار یا آزاد شدن منبع برای همه مصرف‌کننده‌ها).

به مثال زیر توجه کنید :

package main

import (
	"fmt"
	"sync"
)

var sharedResource = make(map[string]interface{})

func main() {
	var wg sync.WaitGroup
	wg.Add(2)
	locker := sync.Mutex{}
	condition := sync.NewCond(&locker)

	go waitForResourceUpdate(&wg, condition, "rsc1")
	go waitForResourceUpdate(&wg, condition, "rsc2")

	// this one writes changes to sharedResource
	condition.L.Lock()
	sharedResource["rsc1"] = "a string"
	sharedResource["rsc2"] = 123456
	condition.Broadcast()
	condition.L.Unlock()

	wg.Wait()
}

// waitForResourceUpdate waits for a signal that a resource changed and prints it.
func waitForResourceUpdate(wg *sync.WaitGroup, cond *sync.Cond, key string) {
	defer wg.Done()
	cond.L.Lock()
	for len(sharedResource) == 0 {
		cond.Wait()
	}
	fmt.Println("Resource", key, ":", sharedResource[key])
	cond.L.Unlock()
}