التزامن في C#: كيف تجعل برنامجك يعمل بكفاءة
\n\nتخيل أنك طاهٍ محترف. لديك ثلاثة مواقد، فرن، وخلاط. إذا قمت بكل شيء بالتسلسل - تقطيع البصل أولاً، ثم القلي، ثم خفق الكريمة - فسيستغرق إعداد الغداء وقتًا طويلاً. الأكثر فعالية هو تشغيل الفرن أثناء تقطيع الخضار، وخفق الكريمة أثناء طهي اللحم. هذا هو مفهوم التزامن - القدرة على تنفيذ مهام متعددة "في وقت واحد" عن طريق التبديل بينها بحيث لا تبقى الموارد خاملة.
\n\nفي لغة C#، هناك ثلاث أدوات رئيسية لتحقيق ذلك: الخيوط التقليدية (Thread)، المهام الحديثة (Task) مع async/await، ومفهوم الجوروتينات المنفذ عبر المكررات (iterators). في هذا المقال، سنشرح كل نهج، ونقارن أداءها، ونوضح متى نستخدم كل منها. إذا كنت تكتب كودًا بلغة C# وتريد أن يكون سريعًا وفعالًا، فأنت في المكان الصحيح.
\n\nالخيوط (Thread): المدفعية الثقيلة
\n\nالخيط هو الطريقة الأكثر تدنيًا لتنظيم التزامن. كل خيط هو "عملية خفيفة" منفصلة داخل برنامجك، لها مكدس (stack) وسياق (context) خاص بها. إنشاء خيط في C# بسيط جدًا:
\n\nusing System;\nusing System.Threading;\n\nclass Program\n{\n static void Main()\n {\n Thread thread = new Thread(DoWork);\n thread.Start();\n Console.WriteLine("الخيط الرئيسي يعمل");\n thread.Join(); // انتظار الانتهاء\n }\n\n static void DoWork()\n {\n for (int i = 0; i < 5; i++)\n {\n Console.WriteLine($"الخيط: {i}");\n Thread.Sleep(200);\n }\n }\n}\n\nهنا قمنا بإنشاء خيط منفصل يقوم بالعد حتى خمسة، بينما يقوم الخيط الرئيسي بطباعة رسالة. بدون Join، قد ينتهي البرنامج قبل أن يكتمل العد.
عيوب الخيوط
\n- \n
- ثقيلة الوزن. كل خيط يحجز حوالي 1 ميجابايت من الذاكرة للمكدس. إنشاء 1000 خيط يعني شغل 1 جيجابايت من الذاكرة دون داعٍ. \n
- صعوبة التزامن. لمنع خيطين من إتلاف البيانات المشتركة، تحتاج إلى أقفال (locks)، وميوتكسات (mutexes)، وسيمفورات (semaphores). خطأ واحد يؤدي إلى deadlock أو race condition. \n
- تبديل السياق. نظام التشغيل يستهلك وقتًا في التبديل بين الخيوط. إذا كان عدد الخيوط أكبر من عدد أنوية المعالج، يصبح هذا عنق زجاجة. \n
الخيوط جيدة عندما تحتاج إلى توازٍ حقيقي على معالج متعدد النواة للحسابات الثقيلة. لكنها زائدة عن الحاجة لعمليات الإدخال/الإخراج (التعامل مع الملفات، الشبكة، قواعد البيانات).
\n\nTask و async/await: المعيار الحديث
\n\nبدءًا من .NET 4.5، قدمت Microsoft بديلًا أنيقًا - مكتبة المهام المتوازية (TPL) والكلمات المفتاحية async/await. هذه ليست خيوطًا بالمعنى التقليدي، بل وعود (promises): تخبر المترجم "قم بتشغيل هذه المهمة، وعندما تنتهي، أعد النتيجة". أثناء تنفيذ المهمة، لا يتم حظر الخيط، بل يعود إلى مجمع الخيوط (thread pool).
using System;\nusing System.Net.Http;\nusing System.Threading.Tasks;\n\nclass Program\n{\n static async Task Main()\n {\n Console.WriteLine("بدء التحميل...");\n string data = await DownloadDataAsync();\n Console.WriteLine($"تم التحميل: {data.Length} حرفًا");\n }\n\n static async Task<string> DownloadDataAsync()\n {\n using HttpClient client = new HttpClient();\n string result = await client.GetStringAsync("https://example.com");\n return result;\n }\n}\n\nلاحظ أن await لا يحجب الخيط. أثناء تحميل البيانات عبر الشبكة، يعود الخيط إلى المجمع ويمكنه خدمة طلبات أخرى. هذا مثالي لخوادم الويب وتطبيقات الواجهات الرسومية - لا تتجمد الواجهة.
كيف يعمل هذا تحت الغطاء؟
\nعندما يرى المترجم async، يحول الطريقة إلى آلة حالة محدودة (state machine). عند كل await، "تتذكر" الطريقة حالتها