9.4.15.1 توضیحات #
الگوی صف (Queue Pattern) در زبان Go، الگویی است که در آن با استفاده از یک goroutine مرکزی و یک یا چند کانال ورودی و خروجی، دادهها را به صورت منظم، به ترتیب ورود (FIFO) مدیریت میکند. در این الگو، برخلاف استفاده مستقیم از کانال که ممکن است ترتیب یا بافر محدودی داشته باشد، یک گوروتین به عنوان صف درونساخت (in-memory queue) عمل میکند و دادههای دریافتی از کانال ورودی را در یک ساختار صف مانند (مانند slice) نگه میدارد، سپس بر اساس منطق زمانبندی یا در دسترس بودن مصرفکننده، آنها را به کانال خروجی ارسال میکند.
هدف اصلی این الگو، کنترل جریان (flow control) و جداسازی سرعت تولید و مصرف دادهها است. برای مثال، اگر producer داده را با سرعت بالایی ارسال کند ولی consumer نتواند به همان سرعت پردازش کند، صف میتواند به عنوان بافر موقت بین این دو عمل کند. این کار از بلاک شدن producer جلوگیری کرده و سیستم را پایدار نگه میدارد. همچنین، چون دادهها در یک ساختار مشخص ذخیره میشوند، میتوان بر ترتیب دریافت، اولویتبندی، یا حتی سیاستهای پردازش (مثل batch processing) نیز کنترل داشت.
این الگو در طراحی سیستمهای message queue، task queue، buffering systems و job dispatcher بسیار رایج است. بهویژه زمانی که لازم باشد چندین درخواست به صف وارد شده و بر اساس اولویت یا نوبت پردازش شوند، یا بین سرعتهای نامتوازن تولید و مصرف تطابق ایجاد شود. الگوی صف در Go، با کمک ترکیب سادهای از goroutine + channel + slice، یک راهکار سبک، قابلاتکا و توسعهپذیر برای این سناریوها ارائه میدهد.
9.4.15.2 دیاگرام #
9.4.15.3 نمونه کد #
1package main
2
3import (
4 "fmt"
5 "time"
6)
7
8func main() {
9 enqueue := make(chan int) // کانال برای دریافت دادههای جدید
10 dequeue := make(chan int) // کانال برای ارسال داده به مصرفکننده
11 done := make(chan struct{}) // کانال برای پایان اجرای برنامه
12
13 // Goroutine صف: مسئول بافر و انتقال دادهها به ترتیب
14 go func() {
15 var queue []int
16 for {
17 var first int
18 var out chan int
19
20 if len(queue) > 0 {
21 first = queue[0]
22 out = dequeue
23 }
24
25 select {
26 case item := <-enqueue:
27 queue = append(queue, item)
28 fmt.Println("Enqueued:", item)
29 case out <- first:
30 queue = queue[1:]
31 case <-done:
32 close(dequeue)
33 return
34 }
35 }
36 }()
37
38 // Producer: ارسال ۱۰ مقدار به صف
39 go func() {
40 for i := 0; i < 10; i++ {
41 enqueue <- i
42 time.Sleep(100 * time.Millisecond)
43 }
44 // پایان
45 time.Sleep(1 * time.Second)
46 done <- struct{}{}
47 }()
48
49 // Consumer: دریافت مقادیر به ترتیب
50 for item := range dequeue {
51 fmt.Println("Dequeued:", item)
52 }
53}
1$ go run main.go
2Enqueued: 0
3Dequeued: 0
4Enqueued: 1
5Dequeued: 1
6Enqueued: 2
7Dequeued: 2
8Enqueued: 3
9Dequeued: 3
10Enqueued: 4
11Dequeued: 4
12Enqueued: 5
13Dequeued: 5
14Enqueued: 6
15Dequeued: 6
16Enqueued: 7
17Dequeued: 7
18Enqueued: 8
19Dequeued: 8
20Enqueued: 9
21Dequeued: 9
در این مثال ما سه کانال داریم: enqueue
برای وارد کردن آیتمها به صف، dequeue
برای خارج کردن آیتمها از صف، و done
برای پایان دادن به اجرای برنامه. این ساختار به ما اجازه میدهد یک صف ساده ولی همزمان و thread-safe را با استفاده از ویژگیهای زبان Go پیادهسازی کنیم.
یک goroutine اصلی مسئول مدیریت صف است. این goroutine یک queue
از نوع []int
نگه میدارد که همان بافر داخلی صف ماست. درون یک حلقه بینهایت، ابتدا بررسی میشود که آیا صف خالی نیست. اگر خالی نبود، مقدار اول صف (first = queue[0]
) برای ارسال آماده میشود و کانال out
برابر dequeue
قرار میگیرد. در غیر این صورت، مقدار out
خالی میماند و بنابراین حالت ارسال انجام نخواهد شد.
سپس با select
سه حالت بررسی میشود. اگر آیتم جدیدی از طریق enqueue
وارد شود، به انتهای صف اضافه میشود. اگر صف خالی نباشد و dequeue
آمادگی دریافت داشته باشد (out <- first
)، مقدار اول صف به مصرفکننده ارسال میشود و از صف حذف میشود (queue = queue[1:]
). اگر سیگنالی از done
برسد، یعنی برنامه باید پایان یابد؛ در این صورت کانال dequeue
بسته میشود و goroutine متوقف میشود.
در بخش Producer، یک goroutine جدید راهاندازی میشود که در آن با استفاده از یک حلقه for
از ۰ تا ۹ مقدار تولید میشود و هر مقدار از طریق enqueue
وارد صف میشود. بعد از پایان تولید دادهها، یک ثانیه صبر میکند و سپس سیگنال done
ارسال میشود تا صف به طور مرتب بسته شود.
در نهایت، حلقه for item := range dequeue
در تابع اصلی (main
) نقش Consumer را بازی میکند. این حلقه از روی کانال dequeue
آیتمها را دریافت میکند و چاپ میکند. از آنجا که این حلقه تا زمان بسته شدن کانال ادامه دارد، بهصورت خودکار پس از رسیدن سیگنال done
و بسته شدن dequeue
، خاتمه مییابد.
در مجموع، این کد پیادهسازی بسیار مناسبی از صف همزمان (synchronized queue) در زبان Go است که از مزیتهای goroutine
ها و channel
ها برای جداسازی concerns و مدیریت همزمانی بهره برده. طراحی آن بسیار ایمن، ساده و مقیاسپذیر است و بهخوبی نشان میدهد چگونه میتوان سیستمهایی با تولیدکننده و مصرفکننده را بدون نیاز به lock و mutex ساخت.
9.4.15.4 کاربردها #
- زمانبندی وظایف (Task Scheduling):
از کانال بهعنوان صف وظایف (task queue) استفاده میشود تا وظایف تولیدشده از سوی گوروتینهای مختلف، به گوروتینهای worker برای اجرا سپرده شوند. این الگو برای پیادهسازی worker pool بسیار رایج است و باعث میشود وظایف به ترتیبی که وارد کانال میشوند پردازش شوند، بدون نیاز به مدیریت پیچیدهی همزمانی با mutex. - بافر کردن دادهها (Buffering Input Data):
در مواقعی که نرخ ورود دادهها بیشتر از نرخ پردازش است، یک کانال بافردار میتواند بهعنوان صف موقت برای ذخیرهی دادهها استفاده شود. این کمک میکند فشار از روی گوروتینی که داده را مصرف میکند برداشته شود و از data loss یا race conditions جلوگیری شود. - محدودسازی نرخ (Throttling/Rate Limiting):
کانال با ظرفیت مشخص میتواند برای کنترل نرخ پردازش به کار رود. اگر گوروتین مصرفکننده کند عمل کند و کانال پر شود، گوروتین تولیدکننده تا زمان آزاد شدن بافر مسدود میماند. این یک روش ساده و کارآمد برای جلوگیری از overload شدن سیستم در شرایط پرترافیک است. - مدیریت گزارشها (Asynchronous Logging):
استفاده از یک کانال بهعنوان صف برای ثبت گزارشها (logs) باعث میشود عملیات نوشتن گزارش (که ممکن است کند باشد) گوروتینهای دیگر را متوقف نکند. گوروتینی که مسئول نوشتن گزارش است پیامها را به ترتیب از کانال دریافت میکند و در فایل یا خروجی شبکه ذخیره میکند، بدون اینکه نیاز به قفل یا ساختار همزمانی پیچیده داشته باشد. - همزمانی امن بین گوروتینها (Safe Inter-Goroutine Communication):
کانالها راهی امن و idiomatic برای تبادل داده بین گوروتینها هستند. استفاده از آنها بهعنوان صف، امکان پیادهسازی سیستمهایی مانند pipeline processing، fan-in/fan-out یا load balancing را به سادگی فراهم میکند، بدون نیاز به primitives سطح پایینتر مثل mutex یا condition variable.