مقدمة: لماذا نحتاج إلى قوائم المهام في Go؟
في تطوير الواجهة الخلفية الحديثة، غالبًا ما تظهر مهام لا تتطلب استجابة فورية للمستخدم: إرسال البريد الإلكتروني، إنشاء التقارير، معالجة الصور، المزامنة مع واجهات برمجة التطبيقات الخارجية. تنفيذ هذه العمليات بشكل متزامن ضمن طلب HTTP هو ممارسة سيئة، لأنه يبطئ الاستجابة ويزيد الحمل على الخادم.
قوائم المهام (job queues) تحل هذه المشكلة. في Go (Golang)، تسمح آليات التزامن المدمجة — goroutines والقنوات — ببناء قوائم موثوقة وفعالة دون اعتماديات خارجية. في هذه المقالة، سنشرح كيفية إنشاء قائمة مهام من الصفر، وتحسينها باستخدام worker pool وتجنب الأخطاء النموذجية.
الأساسيات: goroutines والقنوات
توفر Go بدائيين رئيسيين للعمل مع القوائم:
- Goroutines — خيوط خفيفة يتم تشغيلها باستخدام الكلمة المفتاحية
go. - القنوات — "أنابيب" محددة النوع لنقل البيانات بين goroutines.
أبسط قائمة هي goroutine تقرأ من قناة وتعالج المهام:
package main
import ( "fmt" "time")
func worker(jobs <-chan int) { for job := range jobs { fmt.Printf("معالجة المهمة %d", job) time.Sleep(1 * time.Second) // محاكاة العمل }}
func main() { jobs := make(chan int, 10)
// تشغيل العامل go worker(jobs)
// إرسال المهام for i := 1; i <= 5; i++ { jobs <- i }
close(jobs) // إغلاق القناة بعد إرسال جميع المهام time.Sleep(2 * time.Second)}هذا الكود ينشئ عاملاً واحدًا يعالج المهام بالتسلسل. ولكن للمشاريع الحقيقية، نحتاج إلى قابلية التوسع — عدة عمال.
تنفيذ Worker Pool في Go
Worker pool (مجموعة العمال) هو نمط حيث نقوم بتشغيل عدد ثابت من goroutines العمال، كل منها يأخذ المهام من قناة مشتركة. هذا يسمح بالتحكم في درجة التوازي والحمل على النظام.
التنفيذ الأساسي
package main
import ( "fmt" "sync" "time")
func worker(id int, jobs <-chan int, wg *sync.WaitGroup) { defer wg.Done() for job := range jobs { fmt.Printf("العامل %d: المهمة %d", id, job) time.Sleep(500 * time.Millisecond) }}
func main() { const numWorkers = 3 const numJobs = 10
jobs := make(chan int, numJobs) var wg sync.WaitGroup
// تشغيل العمال for i := 1; i <= numWorkers; i++ { wg.Add(1) go worker(i, jobs, &wg) }
// إرسال المهام for j := 1; j <= numJobs; j++ { jobs <- j } close(jobs)
wg.Wait() // انتظار انتهاء جميع العمال fmt.Println("تم تنفيذ جميع المهام")}هنا نستخدم sync.WaitGroup لانتظار انتهاء جميع العمال. القناة ذات المخزن المؤقت numJobs تسمح بإرسال جميع المهام دون حظر، بينما لم يتمكن العمال من استلامها بعد.
إضافة المهام ديناميكيًا
في التطبيقات الحقيقية، تصل المهام بشكل غير متزامن — على سبيل المثال، عبر HTTP. إليك مثال مع معالج:
package main
import ( "fmt" "net/http" "sync" "time")
type Task struct { ID int Data string}
var ( taskQueue = make(chan Task, 100) wg sync.WaitGroup)
func worker(id int) { defer wg.Done() for task := range taskQueue { fmt.Printf("[العامل %d] معالجة: %s (ID=%d)", id, task.Data, task.ID) time.Sleep(1 * time.Second) }}
func handler(w http.ResponseWriter, r *http.Request) { // الحصول على المهمة من الطلب (بشكل مبسط) task := Task{ID: time.Now().Nanosecond(), Data: "مهمة جديدة"} taskQueue <- task w.W