مقدمة: لماذا نحتاج إلى قوائم المهام في Go؟
\nفي التطوير الحديث، غالبًا ما تنشأ الحاجة إلى معالجة المهام بشكل غير متزامن دون حظر التدفق الرئيسي للتطبيق. قوائم المهام (task queues) هي نمط كلاسيكي يسمح بتوزيع العمل بين عدة معالجات (عمال)، وإدارة الحمل، وضمان تحمل الأخطاء.
\nتوفر لغة Go (Golang) آليات مدمجة لتنفيذ قوائم المهام: الغوروتينات (goroutines) و القنوات (channels). في هذه المقالة، سنقوم عمليًا ببناء قائمة مهام موثوقة باستخدام المكتبة القياسية لـ Go، دون اللجوء إلى وسطاء الرسائل الخارجيين (على الرغم من أن RabbitMQ أو Redis قد تكون مفيدة في بيئة الإنتاج).
\n\nالجزء الرئيسي
\n\n1. النمط الأساسي لمجموعة العمال (Worker Pool)
\nمجموعة العمال هي مجموعة من عدد ثابت من الغوروتينات (العمال) التي تستمع إلى قناة واحدة وتعالج المهام الواردة. يسمح هذا بالتحكم في درجة التوازي وتجنب التحميل الزائد على النظام.
\nلننظر إلى مثال بسيط: نقوم بإنشاء مجموعة من 3 عمال تقوم بمعالجة الأعداد الصحيحة (محاكاة للعمل).
\npackage main\n\nimport (\n "fmt"\n "sync"\n "time"\n)\n\nfunc worker(id int, jobs <-chan int, wg *sync.WaitGroup) {\n defer wg.Done()\n for job := range jobs {\n fmt.Printf("العامل %d بدأ المهمة %d\\n", id, job)\n time.Sleep(time.Second) // محاكاة العمل\n fmt.Printf("العامل %d أكمل المهمة %d\\n", id, job)\n }\n}\n\nfunc main() {\n const numJobs = 5\n const numWorkers = 3\n\n jobs := make(chan int, numJobs)\n var wg sync.WaitGroup\n\n // تشغيل العمال\n for w := 1; w <= numWorkers; w++ {\n wg.Add(1)\n go worker(w, jobs, &wg)\n }\n\n // إرسال المهام\n for j := 1; j <= numJobs; j++ {\n jobs <- j\n }\n close(jobs) // إغلاق القناة لإنهاء العمال\n\n wg.Wait()\n fmt.Println("تم تنفيذ جميع المهام")\n}\nتحليل الكود:
\n- \n
jobs— قناة مخزنة مؤقتًا نرسل إليها المهام. \n worker— دالة تقرأ من القناة وتعالج كل مهمة. \n sync.WaitGroup— تنتظر اكتمال جميع العمال. \n - بعد إرسال جميع المهام، يتم إغلاق القناة (
close(jobs))، مما يؤدي إلى إنهاء حلقةfor rangeفي العمال. \n
هذا النمط هو الأساس للحلول الأكثر تعقيدًا.
\n\n2. قائمة ذات أولويات ومهلات زمنية
\nفي التطبيقات الحقيقية، قد يكون للمهام أولويات مختلفة أو قد تتطلب التنفيذ خلال فترة زمنية محددة. سنقوم بتنفيذ قائمة حيث تتم معالجة المهام ذات الأولوية أولاً، بالإضافة إلى إضافة سياق بمهلة زمنية لإلغاء المهام الطويلة.
\npackage main\n\nimport (\n "context"\n "fmt"\n "time"\n)\n\ntype Task struct {\n ID int\n Priority int // كلما كان الرقم أصغر، زادت الأولوية\n Data string\n}\n\nfunc processTask(ctx context.Context, task Task) error {\n select {\n case <-ctx.Done():\n return ctx.Err()\n case <-time.After(time.Duration(task.Priority) * 500 * time.Millisecond):\n fmt.Printf("تمت معالجة المهمة %d: %s (الأولوية %d)\\n", task.ID, task.Data, task.Priority)\n return nil\n }\n}\n\nfunc main() {\n tasks := []Task{\n {ID: 1, Priority: 3, Data: "أولوية منخفضة"},\n {ID: 2, Priority: 1, Data: "أولوية عالية"},\n {ID: 3, Priority: 2, Data: "أولوية متوسطة"},\n }\n\n // ترتيب حسب الأولوية (الأعلى أولاً)\n // في المشروع الحقيقي، استخدم الترتيب أو الكومة (container/heap)\n for i := 0; i < len(tasks); i++ {\n for j := i + 1; j < len(tasks); j++ {\n if tasks[j].Priority < tasks[i].Priority {\n tasks[i], tasks[j] = tasks[j], tasks[i]\n }\n