9.4.7 الگو Semaphore

9.4.7 الگو Semaphore

9.4.7.1 توضیحات #

الگوی Semaphore (سمیفور) یکی از مفاهیم کلیدی در دنیای همزمانی (Concurrency) است و نقش آن مدیریت کنترل دسترسی به منابع محدود (مانند فایل، شبکه، دیتابیس و…) در یک زمان است. این الگو مخصوصاً زمانی کاربرد دارد که چندین goroutine یا درخواست به طور همزمان قصد استفاده از یک منبع یا سرویس را دارند، اما تنها تعداد محدودی مجاز به استفاده همزمان از آن هستند. پیاده‌سازی این الگو در Go بسیار ساده و idiomatic است و معمولاً از کانال بافر دار (buffered channel) به عنوان سمیفور استفاده می‌شود.

فرض کنید سرور شما قرار است همزمان به ۱۰۰ درخواست HTTP پاسخ دهد؛ اگر همه این درخواست‌ها به طور موازی و بدون کنترل وارد مرحله پردازش شوند، مصرف منابع شبکه (یا سایر منابع اشتراکی) افزایش یافته و به سرعت به نقطه بحرانی می‌رسد که عملکرد سیستم به شدت کاهش پیدا می‌کند و حتی ممکن است به خطا یا اختلال بیانجامد. با استفاده از الگوی سمیفور، می‌توانید تعداد goroutineهای فعال و مشغول به پردازش همزمان را به عددی مشخص (مثلاً ۲۰ یا ۵۰) محدود کنید. این کار موجب می‌شود که منابع با ثبات بیشتری مورد استفاده قرار گیرند و بار اضافی و سربار مدیریت سیستم کاهش یابد.

در پیاده‌سازی این الگو در Go، یک کانال بافر دار (مثلاً make(chan struct{}, 20)) به عنوان سمیفور تعریف می‌شود. هر goroutine قبل از شروع پردازش، یک مقدار (مثلاً struct{} یا هر مقدار دلخواه) در کانال قرار می‌دهد. اگر کانال پر باشد، goroutine جدید بلاک می‌شود تا زمانی که جای خالی ایجاد شود. پس از پایان کار، goroutine مقدار خود را از کانال خارج می‌کند تا اجازه فعالیت به goroutine دیگری داده شود. این تکنیک همزمانی ایمن و کنترل‌شده را فراهم می‌کند و به راحتی قابل توسعه و مقیاس‌پذیر است.

سمیفور برای سناریوهای دیگری مانند مدیریت همزمان دسترسی به پایگاه داده، خواندن/نوشتن فایل‌ها، کنترل اجرای Taskهای سنگین و حتی مدیریت connection poolها نیز استفاده می‌شود و یکی از مهم‌ترین ابزارهای جلوگیری از overload شدن سیستم و حفظ پایداری نرم‌افزارهای concurrent است. استفاده از کانال بافر دار به عنوان سمیفور، یک راه حل idiomatic و ساده برای پیاده‌سازی این کنترل در زبان Go محسوب می‌شود و اغلب در کدهای تولیدی مشاهده می‌شود.

به نقل از ویکی پدیا :

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

سمافورها اولین بار به‌وسیلهٔ دانشمند علوم رایانه هلندی، ادسخر دیکسترا معرفی شدند.[۱] و امروزه به‌طور گسترده‌ای در سیستم عاملها مورد استفاده قرار می‌گیرند.

9.4.7.2 دیاگرام #

Semaphore

9.4.7.3 نمونه کد #

 1package main
 2
 3import (
 4	"fmt"
 5	"sync"
 6	"time"
 7)
 8
 9// Interface optional, usually direct struct is enough
10type Semaphore struct {
11	semCh chan struct{}
12}
13
14func NewSemaphore(maxConcurrency int) *Semaphore {
15	return &Semaphore{
16		semCh: make(chan struct{}, maxConcurrency),
17	}
18}
19
20func (s *Semaphore) Acquire() {
21	s.semCh <- struct{}{}
22}
23
24func (s *Semaphore) Release() {
25	<-s.semCh
26}
27
28func main() {
29	maxConcurrent := 3
30	totProcess := 10
31
32	sem := NewSemaphore(maxConcurrent)
33	var wg sync.WaitGroup
34
35	for i := 1; i <= totProcess; i++ {
36		wg.Add(1)
37		sem.Acquire()
38		go func(taskID int) {
39			defer wg.Done()
40			defer sem.Release()
41			longRunningProcess(taskID)
42		}(i)
43	}
44
45	wg.Wait()
46	fmt.Println("All tasks finished!")
47}
48
49func longRunningProcess(taskID int) {
50	fmt.Println(time.Now().Format("15:04:05"), "Running task", taskID)
51	time.Sleep(2 * time.Second)
52}
 1$ go run main.go
 223:00:00 Running task 3
 323:00:00 Running task 1
 423:00:00 Running task 2
 523:00:02 Running task 4
 623:00:02 Running task 5
 723:00:02 Running task 6
 823:00:04 Running task 7
 923:00:04 Running task 9
1023:00:04 Running task 8
1123:00:06 Running task 10
12All tasks finished!

در این نسخه بهبود یافته از الگوی Semaphore، هدف کنترل تعداد goroutineهای همزمان و اطمینان از اجرای کامل تمام وظایف (tasks) بدون هیچ‌گونه race condition یا مشکل همزمانی است. در ابتدا با ساخت یک struct ساده به نام Semaphore و تعریف یک کانال بافر دار به اندازه‌ی تعداد مجاز عملیات همزمان (در اینجا ۳)، یک Semaphore سبک اما مؤثر ساخته می‌شود. هر زمان که یک goroutine می‌خواهد اجرا شود، ابتدا باید یک اسلات در این کانال اشغال کند (Acquire). اگر ظرفیت کانال پر باشد، goroutine تا آزاد شدن یک اسلات جدید منتظر می‌ماند. پس از پایان کار، با دستور Release اسلات آزاد می‌شود تا goroutine بعدی بتواند اجرا شود.

در تابع main یک حلقه وظیفه راه‌اندازی ۱۰ goroutine را دارد، ولی با کمک Semaphore فقط ۳ کار همزمان می‌توانند در هر لحظه فعال باشند. برای اطمینان از اینکه تمام goroutineها به‌درستی اجرا و پایان یافته‌اند، از sync.WaitGroup استفاده شده است: قبل از راه‌اندازی هر goroutine مقدار WaitGroup افزایش و پس از اتمام آن کاهش می‌یابد. در انتها با دستور wg.Wait() مطمئن می‌شویم که برنامه فقط پس از اتمام همه کارها به پایان می‌رسد. این مکانیزم از خروج زودهنگام main یا رخ دادن goroutine leak جلوگیری می‌کند.

هر goroutine یک تابع شبیه‌ساز کار سنگین (longRunningProcess) را با شناسه‌ی خود اجرا می‌کند که خروجی اجرای task و زمان شروع آن را در لاگ چاپ می‌کند و با یک توقف (sleep) دو ثانیه‌ای، بار واقعی‌تری ایجاد می‌نماید. این پیاده‌سازی تضمین می‌کند که همزمانی به‌شکلی کنترل‌شده انجام شود، تعداد goroutineها بیش از حد نشود و سرور یا سیستم هیچ‌گاه overloaded نشود. همین الگو در بسیاری از سناریوهای واقعی مثل دانلود فایل، فراخوانی APIهای موازی، پردازش صف داده و مدیریت connection pool استفاده می‌شود و پایه‌ی معماری بسیاری از سرویس‌های مقیاس‌پذیر است.
همچنین این روش idiomatic Go است و برای توسعه در پروژه‌های تولیدی کاملاً توصیه می‌شود.

9.4.7.4 کاربردها #

  • مدیریت دسترسی به منابع مشترک (Shared Resource Management): سمیفور برای محدود کردن تعداد goroutineهایی که همزمان به یک منبع مشترک (مانند فایل، دیتابیس، یا یک سرویس خارجی) دسترسی دارند، استفاده می‌شود. این کنترل از بروز شرایط رقابتی (race conditions) و مصرف بیش از حد منابع جلوگیری می‌کند و پایداری سیستم را تضمین می‌کند.
  • همگام‌سازی و کنترل دسترسی به ساختار داده‌های اشتراکی (Data Structure Synchronization): زمانی که چندین goroutine نیاز دارند به طور همزمان روی یک ساختار داده مانند map، queue یا cache کار کنند، می‌توان با سمیفور تعداد عملیات همزمان روی آن ساختار را محدود کرد تا همزمانی ایمن و مدیریت‌شده داشته باشیم.
  • مدیریت منابع محدود (Limited Resource Allocation): بسیاری از منابع سیستم مانند connection pool دیتابیس، worker pool، پردازشگرهای شبکه یا حتی ظرفیت نوشتن روی دیسک دارای محدودیت فیزیکی یا منطقی هستند. سمیفور تضمین می‌کند که هرگز بیش از تعداد مشخصی از این منابع همزمان اشغال نشوند.
  • پیاده‌سازی Load Balancer یا Rate Limiter: می‌توانید از سمیفور برای کنترل تعداد درخواست‌های همزمان که از طریق یک load balancer یا API gateway به سرویس اصلی ارسال می‌شوند استفاده کنید. این کار کمک می‌کند سرویس پشت‌صحنه هیچ‌وقت overload نشود و کیفیت پاسخ‌دهی به کاربران حفظ شود. همچنین می‌توان با همین الگو، الگوریتم‌های rate limiting پیاده‌سازی کرد.
  • پیاده‌سازی Worker Pool یا Thread Pool: سمیفور هسته‌ی معماری Worker Pool است؛ یعنی تعداد taskهای فعال (یا thread/goroutine) در هر لحظه را محدود می‌کند و باعث می‌شود که هرگز بیش از ظرفیت واقعی سیستم، کار موازی اجرا نشود. این روش در سیستم‌های پردازش موازی، صف‌بندی background jobها، و بهبود performance به شدت کاربردی است.
  • کنترل ترافیک ورودی یا API Throttling: با استفاده از سمیفور می‌توان تعداد پذیرش درخواست‌های همزمان ورودی (مثلاً به یک endpoint حیاتی یا سرویس خاص) را محدود کرد و در شرایط overload، درخواست‌های اضافی را reject یا queue کرد تا سیستم همیشه پاسخگو و پایدار بماند.