مقدمة: لماذا نحتاج إلى قوائم المهام في C#؟
\nفي التطوير الحديث باستخدام C# (.NET)، غالبًا ما تنشأ الحاجة إلى تنظيم معالجة البيانات غير المتزامنة. المثال الكلاسيكي هو خادم الويب الذي يستقبل الطلبات ويجب معالجتها في الخلفية دون حظر الخيط الرئيسي. أو تطبيق يقوم بتنزيل الملفات من الإنترنت ويعالج الملفات التي تم تنزيلها بالفعل بالتوازي. لمثل هذه السيناريوهات، يعتبر نمط «المنتج-المستهلك» مثاليًا، وأساسه هو قائمة المهام.
\nتسمح قائمة المهام بنقل العمل بأمان من خيط (أو عملية غير متزامنة) إلى آخر، مع مزامنة الوصول إلى البيانات المشتركة. في هذه المقالة، سننظر في ثلاث طرق رئيسية لتنفيذ القوائم في C#: ConcurrentQueue و BlockingCollection و System.Threading.Channels الحديثة. كل منها مناسب لسيناريوهات مختلفة، وسنشرحها بأمثلة عملية.
1. ConcurrentQueue — قائمة انتظار أساسية آمنة للخيوط
\nConcurrentQueue<T> هي أبسط قائمة انتظار آمنة للخيوط من مساحة الأسماء System.Collections.Concurrent. تستخدم خوارزميات خالية من القفل (المقارنة والتبادل، CAS) لضمان أداء عالٍ. ومع ذلك، فهي لا تحجب الخيط عندما تكون القائمة فارغة — سيتعين عليك تنفيذ الانتظار بنفسك.
مثال: تحميل البيانات من مصادر متعددة
\nلنفترض أن لدينا مهمة: تنزيل 10 صفحات ويب ومعالجتها. خيط واحد (المنتج) يضيف عناوين URL إلى القائمة، بينما عدة خيوط (المستهلكون) تسحبها وتنزلها.
\nusing System;\nusing System.Collections.Concurrent;\nusing System.Threading.Tasks;\n\npublic class ConcurrentQueueExample\n{\n public static async Task RunAsync()\n {\n var queue = new ConcurrentQueue<string>();\n var urls = new[] { "https://example.com", "https://dotnet.microsoft.com" };\n\n // Producer: إضافة المهام\n foreach (var url in urls)\n {\n queue.Enqueue(url);\n }\n\n // Consumers: معالجة المهام\n var tasks = new Task[3];\n for (int i = 0; i < tasks.Length; i++)\n {\n tasks[i] = Task.Run(() =>\n {\n while (queue.TryDequeue(out var url))\n {\n Console.WriteLine($"معالجة: {url}");\n // محاكاة العمل\n Task.Delay(500).Wait();\n }\n });\n }\n\n await Task.WhenAll(tasks);\n Console.WriteLine("تم تنفيذ جميع المهام.");\n }\n}\nالمزايا: البساطة، أداء عالٍ مع عدد كبير من العمليات.
\nالعيوب: لا توجد آلية انتظار مدمجة (يحتاج إلى حلقة تحقق)، صعوبة تحديد حجم القائمة.
2. BlockingCollection — قائمة انتظار مع حظر وحد أقصى
\nBlockingCollection<T> هي غلاف لمجموعة آمنة للخيوط، تضيف إمكانية حظر خيط المستهلك عندما تكون القائمة فارغة، وخيط المنتج عندما تكون القائمة ممتلئة. افتراضيًا، تستخدم ConcurrentQueue، ولكن يمكن استبدالها بأي مجموعة تطبق IProducerConsumerCollection<T>.
مثال: معالجة الطلبات مع تحديد حجم المخزن المؤقت
\nتخيل نظام معالجة الطلبات. المنتج يولد الطلبات، وثلاثة مستهلكين يعالجونها. المخزن المؤقت محدود بـ 10 عناصر لتجنب استهلاك الكثير من الذاكرة.
\nusing System;\nusing System.Collections.Concurrent;\nusing System.Threading.Tasks;\n\npublic class BlockingCollectionExample\n{\n public static async Task RunAsync()\n {\n using var orders = new BlockingCollection<string>(boundedCapacity: 10);\n\n // Producer\n var producer = Task.Run(() =>\n {\n for (int i = 1; i <= 20; i++)\n