لماذا يتباطأ برنامجك وكيف تصلح ذلك
\n\nتخيل: لقد كتبت محللاً رائعاً بلغة C++ يجب أن يعالج 10,000 عنوان URL. تقوم بتشغيله، وتذهب لتحضر القهوة، وتعود - ولا يزال يقوم بتحميل الموقع الأول. هل هذا مألوف؟ المشكلة هي أن البرنامج يعمل بشكل متزامن: بينما يتم تنفيذ طلب واحد، تنتظر الطلبات الأخرى دورها، كما في متجر سوفييتي.
\n\nفي العالم الحديث، لا يغفر المستخدمون التأخير. إذا استجاب الخادم بأكثر من 200 مللي ثانية - فأنت تفقد العملاء. ولكن كيف تجعل لغة C++ تعمل بشكل أسرع دون إعادة كتابة كل شيء من الصفر؟ الإجابة هي قوائم الانتظار والمهام الخلفية. هذا ليس مجرد مصطلح عصري، بل هو نمط أساسي يُستخدم في نواة لينكس ومحركات الألعاب والخدمات عالية التحميل.
\n\nفي هذه المقالة، سنفهم كيفية عمل قوائم انتظار المهام في لغة C++، ونكتب تجمع خيوط بسيط خاص بنا، ونتعلم كيفية معالجة البيانات في الخلفية دون سباقات البيانات أو حالات الجمود. هل أنت مستعد لتحسين كودك؟ لنبدأ.
\n\nما هي قائمة انتظار المهام ولماذا هي ضرورية
\n\nقائمة انتظار المهام (task queue) هي بنية بيانات تعمل وفق مبدأ FIFO (أول من يدخل، أول من يخرج). تخيل خط تجميع: تضيف المهام من طرف، وتأخذها خيوط التنفيذ من الطرف الآخر. هذا هو أساس نمط المنتج-المستهلك (Producer-Consumer).
\n\nلماذا هذا ضروري في لغة C++؟ إليك ثلاثة أسباب رئيسية:
\n\n- \n
- المعالجة المتوازية - تأخذ عدة خيوط المهام من قائمة الانتظار في وقت واحد، مما يسرع العمل أضعافاً مضاعفة. \n
- البرمجة غير المتزامنة - لا يتم حظر الخيط الرئيسي، بل يستمر في استقبال طلبات جديدة أو تحديث الواجهة. \n
- موازنة التحميل - إذا كانت إحدى المهام ثقيلة، ستلتقط الخيوط الأخرى المهام الخفيفة، ولن يتعطل النظام. \n
على سبيل المثال، يمكن لخادم ويب بلغة C++ وضع طلبات HTTP الواردة في قائمة انتظار، بينما يقوم تجمع الخيوط بمعالجتها بالتوازي. بينما يقوم خيط بتحليل JSON، يكتب آخر في قاعدة البيانات - وكل هذا دون حظر الحلقة الرئيسية.
\n\nكتابة قائمة انتظار بسيطة بلغة C++ من الصفر
\n\nلنبدأ بمثال أساسي. في المكتبة القياسية للغة C++ يوجد std::queue، ولكن لتعدد الخيوط يجب تغليفها في فئة محمية. إليك أبسط تطبيق لقائمة انتظار آمنة للخيوط:
#include <queue>\n#include <mutex>\n#include <condition_variable>\n#include <functional>\n#include <thread>\n\nclass ThreadSafeQueue {\nprivate:\n std::queue<std::function<void()>> tasks;\n std::mutex mtx;\n std::condition_variable cv;\n\npublic:\n void push(std::function<void()> task) {\n std::lock_guard<std::mutex> lock(mtx);\n tasks.push(task);\n cv.notify_one(); // نوقظ خيطاً واحداً منتظراً\n }\n\n std::function<void()> pop() {\n std::unique_lock<std::mutex> lock(mtx);\n cv.wait(lock, [this] { return !tasks.empty(); });\n auto task = tasks.front();\n tasks.pop();\n return task;\n }\n};\n\n\nهنا نستخدم std::mutex لحماية الوصول إلى قائمة الانتظار و std::condition_variable حتى لا تستهلك الخيوط المعالج بلا فائدة أثناء انتظار مهام جديدة. عندما تظهر مهمة، نوقظ خيطاً واحداً باستخدام notify_one().
ولكن هذا فقط نصف العمل. نحتاج إلى تجمع خيوط (thread pool) لتنفيذ هذه المهام.
\n\nإنشاء تجمع خيوط: قلب المهام الخلفية
\n\nتجمع الخيوط هو مجموعة من الخيوط المنشأة مسبقاً والتي تنتظر العمل. بدلاً من إنشاء وتدمير الخيوط لكل مهمة (وهو أمر مكلف)، نقوم بإعادة استخدامها. إليك تطبيق تقليدي:
\n\nclass ThreadPool {\nprivate:\n std::vector<std::thread> workers;\n ThreadSafeQueue queue;\n bool stop = false;\n\n void worker_loop() {\n while (!stop) {\n