مقدمة: لماذا التزامن في C++ ليس مخيفًا، بل مثيرًا للاهتمام؟
\n\nمنذ زمن بعيد، في عصر المعالجات أحادية النواة، كانت البرامج تُنفذ بشكل تسلسلي صارم. اليوم، حتى هاتفك الذكي يحتوي على 8 أنوية، والخوادم تحتوي على عشرات. جعل كل هذه الأنوية تعمل لصالحك - هنا تكمن السحر الحقيقي للأداء. وC++، واحدة من أقوى اللغات وأكثرها انخفاضًا في المستوى، تمنحنا ترسانة كاملة من الأدوات لتحقيق ذلك. ولكن كيف لا نضيع؟ الخيوط، الكوروتينات (غالبًا ما تُسمى "goroutines" تشبيهًا بلغة Go)، async/await - ماذا نختار؟
\n\nفي هذه المقالة، سنقوم بترتيب التزامن الحديث في C++ بشكل منظم. ستتعرف متى تستخدم الخيوط التقليدية، ومتى تستخدم كوروتينات C++20 الخفيفة، وكيف يبسط async/await البرمجة اللاتزامنية. هيا بنا!
\n\nالكلاسيكيات: تعدد الخيوط مع std::thread
\n\nلنبدأ بالأكثر وضوحًا - الخيوط. std::thread هو وصول مباشر إلى خيوط نظام التشغيل. كل خيط هو سياق تنفيذ منفصل يمكنه العمل على نواة CPU منفصلة.
مثال بسيط: حساب متوازٍ
\n\nتخيل أنك بحاجة لحساب مجموع مربعات الأرقام من 1 إلى 10 ملايين. بشكل تسلسلي - هذا طويل. دعنا نوازي:
\n\n#include <iostream>\n#include <thread>\n#include <vector>\n#include <numeric>\n\nvoid sum_squares(int start, int end, long long& result) {\n result = 0;\n for (int i = start; i <= end; ++i) {\n result += i * i;\n }\n}\n\nint main() {\n const int N = 10'000'000;\n const int NUM_THREADS = 4;\n std::vector<std::thread> threads;\n std::vector<long long> results(NUM_THREADS, 0);\n \n int chunk = N / NUM_THREADS;\n for (int i = 0; i < NUM_THREADS; ++i) {\n int start = i * chunk + 1;\n int end = (i == NUM_THREADS - 1) ? N : (i + 1) * chunk;\n threads.emplace_back(sum_squares, start, end, std::ref(results[i]));\n }\n \n for (auto& t : threads) {\n t.join();\n }\n \n long long total = std::accumulate(results.begin(), results.end(), 0LL);\n std::cout << "مجموع المربعات: " << total << std::endl;\n return 0;\n}\n\n\nهنا ننشئ 4 خيوط، كل منها يحسب الجزء الخاص به من النطاق. نجمع النتائج في مصفوفة. كل شيء بسيط ومتوقع. ولكن هناك تفاصيل:
\n\n- \n
- التكاليف الإضافية: إنشاء كل خيط مكلف (تخصيص مكدس، استدعاءات نظام). لآلاف المهام الصغيرة، هذا غير فعال. \n
- المزامنة: إذا وصلت الخيوط إلى بيانات مشتركة، نحتاج إلى أقفال (mutexes) التي قد تسبب حظرًا وتقلل الأداء. \n
- عدد محدود: عدد الخيوط عادة لا يجب أن يتجاوز عدد أنوية CPU (مع مراعاة Hyper-Threading). \n
للحسابات الثقيلة والطويلة، الخيوط هي اختيار ممتاز. ولكن لعمليات الإدخال/الإخراج (قراءة ملفات، طلبات شبكة) غالبًا ما تكون زائدة عن الحاجة. هنا تظهر الكوروتينات على المسرح.
\n\nثورة C++20: الكوروتينات (goroutines)
\n\nالكوروتينات في C++20 ليست مجرد ميزة إضافية. إنها تغيير جوهري في كيفية كتابة الكود اللاتزامني. على عكس الخيوط، الكوروتينات هي برامج فرعية خفيفة الوزن يمكنها تعليق تنفيذها واستئنافه لاحقًا، دون حظر خيط نظام التشغيل.
\n\nيطلق عليها العديد من المطورين اسم "goroutines" بسبب تشابهها مع goroutines في Go، على الرغم من أنها تقنيًا مفاهيم مختلفة. الميزة الرئيسية: يمكنك تشغيل مئات الآلاف من الكوروتينات على خيط واحد دون فقدان الأداء.
\n\nكيف يعمل هذا؟
\n\nالكوروتين هو دالة تحتوي على الكلمة المفتاحية co_await أو co_return أو co_yield. يقوم المترجم تلقائيًا بتحويلها إلى آلة حالة. إليك مثال أساسي:
#include <iostream>\n#include <coroutine>\n#incl