9.4.10 الگو Monitor

9.4.10 الگو Monitor

9.4.10.1 توضیحات #

الگوی مانیتور (Monitor Pattern) یکی از مفاهیم کلیدی در طراحی سیستم‌های همزمان است که هدف آن فراهم کردن مکانیزمی برای مدیریت ایمن و هماهنگ دسترسی چندین goroutine به یک منبع یا وضعیت مشترک است. این الگو به گونه‌ای طراحی شده که goroutineها بتوانند زمانی که منتظر وقوع یک شرط خاص (مثلاً آماده شدن داده یا تغییر وضعیت یک منبع) هستند، بدون مصرف بیهوده منابع (مانند CPU) یا بلاک شدن کل برنامه، به حالت خواب بروند و به محض برقرار شدن شرط، از خواب بیدار شوند و ادامه اجرا دهند. این رفتار دقیقاً چیزی است که در زبان Go می‌توان با کمک ساختار sync.Cond پیاده‌سازی کرد.

ساختار sync.Cond در پکیج sync زبان Go، ابزاری قدرتمند برای پیاده‌سازی این الگو است. یک شیء Cond روی یک lock (مانند sync.Mutex یا sync.RWMutex) ساخته می‌شود و سه متد کلیدی دارد:

  • Wait() که goroutine جاری را به حالت خواب می‌برد تا زمانی که از طریق سیگنال بیدار شود؛
  • Signal() که یکی از goroutineهای منتظر را بیدار می‌کند؛
  • Broadcast() که همه‌ی goroutineهای منتظر را بیدار می‌کند.

هنگامی که goroutine شرط مورد نظرش برقرار نشده، متد Wait را صدا می‌زند و lock را به طور موقت آزاد می‌کند تا دیگران هم بتوانند منبع مشترک را تغییر دهند. پس از دریافت سیگنال و بیدار شدن، دوباره lock را به دست می‌گیرد و شرط را بررسی می‌کند. این روش بسیار ایمن، سریع و idiomatic است و از busy waiting (حلقه‌ی بی‌پایان با مصرف CPU) جلوگیری می‌کند.

الگوی مانیتور با استفاده از sync.Cond معمولاً در سناریوهایی مانند صف‌های تولید-مصرف (زمانی که صف خالی است، مصرف‌کننده منتظر تولید داده می‌ماند)، پیاده‌سازی سیستم‌های صف انتظار (Waiting Queue)، کنترل منابع اشتراکی، یا هرجایی که نیاز به هماهنگی و همزمانی پیشرفته بین goroutineها وجود دارد، استفاده می‌شود. این الگو باعث افزایش پایداری و کارایی سیستم‌های concurrent می‌شود و پیاده‌سازی آن در Go هم ساده و هم بسیار قدرتمند است.

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

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

9.4.10.2 دیاگرام #

flowchart TD A[Goroutine] -->|Check Condition| B{Condition met?} B -- Yes --> C[Process / Continue] B -- No --> D[Wait sync.Cond] D -. Receive Signal .-> E[Wake Up Goroutine] E -->|Acquire lock| B F[Other Goroutines] -->|Signal/Broadcast| D

9.4.10.3 نمونه کد #

 1package main
 2
 3import (
 4	"fmt"
 5	"sync"
 6)
 7
 8type Item = int
 9
10type Queue struct {
11	items  []Item
12	closed bool
13	*sync.Cond
14}
15
16// ایجاد صف جدید
17func NewQueue() *Queue {
18	return &Queue{
19		Cond: sync.NewCond(&sync.Mutex{}),
20	}
21}
22
23// قرار دادن یک آیتم در صف
24func (q *Queue) Put(item Item) error {
25	q.L.Lock()
26	defer q.L.Unlock()
27	if q.closed {
28		return fmt.Errorf("queue is closed")
29	}
30	q.items = append(q.items, item)
31	q.Signal() // فقط یکی از منتظرها را بیدار کن (بهینه‌تر)
32	return nil
33}
34
35// گرفتن n آیتم از صف، یا برگشت آیتم‌های موجود در صورت بسته بودن صف
36func (q *Queue) GetMany(n int) ([]Item, error) {
37	q.L.Lock()
38	defer q.L.Unlock()
39	for len(q.items) < n && !q.closed {
40		q.Wait()
41	}
42	if len(q.items) == 0 && q.closed {
43		return nil, fmt.Errorf("queue closed and empty")
44	}
45	// اگر صف بسته شده و آیتم‌هایی باقی مانده است، همان‌ها را بازگردان
46	m := n
47	if len(q.items) < n {
48		m = len(q.items)
49	}
50	items := q.items[:m:m]
51	q.items = q.items[m:]
52	return items, nil
53}
54
55// بستن صف و بیدار کردن همه goroutineهای منتظر
56func (q *Queue) Close() {
57	q.L.Lock()
58	defer q.L.Unlock()
59	q.closed = true
60	q.Broadcast() // همه منتظرها را بیدار کن
61}
62
63func main() {
64	q := NewQueue()
65	var wg sync.WaitGroup
66
67	// مصرف‌کننده‌ها
68	for n := 10; n > 0; n-- {
69		wg.Add(1)
70		go func(n int) {
71			defer wg.Done()
72			for {
73				items, err := q.GetMany(n)
74				if err != nil {
75					break
76				}
77				fmt.Printf("%2d: %v\n", n, items)
78			}
79		}(n)
80	}
81
82	// تولید داده
83	for i := 0; i < 100; i++ {
84		_ = q.Put(i)
85	}
86	q.Close() // صف را پس از تولید داده می‌بندیم
87
88	wg.Wait()
89	fmt.Println("All done!")
90}
 1$ go run main.go
 2 1: [0]
 3 1: [1]
 4 1: [2]
 5 1: [3]
 6 1: [4]
 7 1: [5]
 8 1: [6]
 9 1: [7]
10 1: [8]
11 1: [9]
12 1: [10]
13 1: [11]
14 1: [12]
15 1: [13]
16 7: [14 15 16 17 18 19 20]
17 7: [43 44 45 46 47 48 49]
18 7: [72 73 74 75 76 77 78]
19 7: [79 80 81 82 83 84 85]
20 7: [86 87 88 89 90 91 92]
21 7: [93 94 95 96 97 98 99]
22 5: [50 51 52 53 54]
23 9: [55 56 57 58 59 60 61 62 63]
24 8: [64 65 66 67 68 69 70 71]
25 1: [21]
26 2: [25 26]
27 3: [22 23 24]
28 6: [27 28 29 30 31 32]
2910: [33 34 35 36 37 38 39 40 41 42]
30All done!

در این مثال، یک صف thread-safe (ایمن برای همزمانی) با استفاده از الگوی مانیتور (Monitor Pattern) و ابزار قدرتمند sync.Cond پیاده‌سازی شده است. این صف، امکان قرار دادن آیتم (توسط تولیدکننده‌ها) و دریافت چند آیتم به صورت همزمان (توسط مصرف‌کننده‌ها) را به‌صورت هماهنگ و ایمن فراهم می‌کند.

در این معماری، متد Put برای افزودن آیتم جدید به صف استفاده می‌شود و با هر بار افزودن، یکی از goroutineهای منتظر (مصرف‌کننده‌ها) را با متد Signal() بیدار می‌کند تا در صورت آماده بودن شرط (یعنی تعداد آیتم کافی)، کار خود را ادامه دهد. در سمت مصرف‌کننده، هر goroutine با متد GetMany(n) منتظر می‌ماند تا حداقل n آیتم در صف موجود شود. اگر این شرط برقرار نباشد و صف همچنان باز باشد، مصرف‌کننده با متد Wait() به حالت خواب می‌رود تا زمانی که داده کافی توسط تولیدکننده وارد صف شود یا صف بسته شود.

نکته کلیدی اینجاست که بعد از اتمام تولید داده (در این مثال پس از افزودن ۱۰۰ آیتم)، با فراخوانی متد Close() صف بسته می‌شود و همه goroutineهای منتظر با Broadcast() بیدار می‌شوند. این کار تضمین می‌کند هیچ مصرف‌کننده‌ای به صورت بی‌نهایت در حالت انتظار نخواهد ماند و همگی graceful و تمیز به کار خود پایان می‌دهند. اگر صف بسته و خالی باشد، مصرف‌کننده‌ها پیام خطا دریافت و خارج می‌شوند.

در نهایت با استفاده از یک sync.WaitGroup اطمینان حاصل می‌شود که تمام goroutineها (مصرف‌کننده‌ها) پس از اتمام واقعی پردازش و بدون هیچ‌گونه goroutine leak یا بن‌بست (deadlock) خاتمه می‌یابند. این معماری، هم مقیاس‌پذیر، هم ایمن، و هم idiomatic در دنیای Go است و می‌تواند در سناریوهای تولید-مصرف، صف‌های پردازش موازی، و حتی سیستم‌های real-time به‌سادگی استفاده شود.

9.4.10.4 کاربردها #

  • پردازش دسته‌ای داده (Batch Processing): زمانی که نیاز دارید تعداد مشخصی داده (مثلاً ۱۰ آیتم) جمع‌آوری و سپس به صورت یکجا پردازش شوند، می‌توانید با استفاده از sync.Cond منتظر بمانید تا شرط “تعداد کافی آیتم در صف” برقرار شود. سپس با سیگنال به مصرف‌کننده‌ها اطلاع می‌دهید که اکنون دسته داده آماده پردازش است و می‌توانند ادامه دهند.
  • انتظار برای وقوع رویدادهای خارجی: اگر لازم است goroutineها تا وقوع یک رویداد خاص (مثل تکمیل عملیات در سرویس خارجی، رسیدن پیام از سرور، پایان کار background یا حتی فشار یک دکمه توسط کاربر) متوقف بمانند، با Cond می‌توانید آن‌ها را به خواب بفرستید تا زمانی که سیگنال یا Broadcast داده شود و همه با هم یا یکی یکی بیدار شوند.
  • کنترل جریان و هماهنگی اجرای goroutineها (Flow Control & Synchronization): در مواقعی که لازم است فقط تعداد مشخصی از goroutineها همزمان وارد بخش بحرانی شوند یا اجرای بخشی از کد فقط پس از رخداد شرایط خاص آغاز شود، می‌توانید با کمک sync.Cond و شرط‌های سفارشی، کنترل کامل اجرای concurrent را داشته باشید (مثلاً: شروع تمام همزمان، یا توقف گروهی هنگام رسیدن به نقطه sink).
  • همگام‌سازی و کنترل قفل‌های منابع مشترک: اگر دسترسی به یک منبع (مثلاً یک بافر یا شیء مشترک) باید با شرایط خاصی صورت گیرد (مثلاً تا زمانی که منبع خالی/پر نشده، اجازه دسترسی داده نشود)، می‌توانید با Cond گوروتین‌های منتظر را تا زمان آزاد شدن قفل یا فراهم شدن شرط، به خواب ببرید و سپس با Signal/Broadcast آنها را بیدار کنید.
  • همگام‌سازی پایان و شروع عملیات چندگانه (Barrier Synchronization): زمانی که چندین goroutine باید همگی یک مرحله کار را به اتمام برسانند تا مرحله بعدی آغاز شود (مانند الگوی barrier)، می‌توان با Cond به هر goroutine پس از اتمام کار سیگنال داد و منتظر ماند تا همه به نقطه هماهنگ برسند، سپس اجرای مرحله بعدی را شروع کرد.
  • انتظار پویا برای داده یا منبع: در صف‌های message queue، اگر مصرف‌کننده‌ها به داده نیاز دارند اما صف خالی است، به جای busy waiting، می‌توانند تا زمان ورود داده با Wait منتظر بمانند و تولیدکننده با Signal مصرف‌کننده‌ها را بیدار کند. این کار کارایی و مصرف منابع را بهبود می‌دهد.