9.4.15 الگو Queuing

9.4.15 الگو Queuing

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 دیاگرام #

flowchart LR Producer1[Producer 1] --> IN[Input Channel] Producer2[Producer 2] --> IN IN --> Q[Queue Goroutine Buffered with slice] Q --> OUT[Output Channel] OUT --> Consumer1[Consumer 1] OUT --> Consumer2[Consumer 2] style IN fill:#d0e8ff,stroke:#2980b9,stroke-width:2px style OUT fill:#d0e8ff,stroke:#2980b9,stroke-width:2px style Q fill:#fff0cc,stroke:#e67e00,stroke-width:2px style Producer1,Producer2 fill:#e6ffe6,stroke:#27ae60,stroke-width:2px style Consumer1,Consumer2 fill:#fde2e2,stroke:#c0392b,stroke-width:2px

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.