التزامن وتعدد الخيوط في C++: من الأساسيات إلى المعايير الحديثة
التطبيقات الحديثة تتطلب بشكل متزايد تنفيذ مهام متعددة في وقت واحد. سواء كان ذلك معالجة طلبات المستخدمين على الخادم، أو عرض الرسومات، أو العمل مع البيانات الضخمة — أصبحت التزامنية وتعدد الخيوط في C++ مهارات أساسية للمطور. في هذه المقالة، سنستعرض الآليات الرئيسية المتاحة في المكتبة القياسية لـ C++، بدءاً من C++11 وحتى C++20. ستتعلم كيفية إنشاء الخيوط، ومزامنة عملها، وتجنب الأخطاء الشائعة.
إذا كنت على دراية بأساسيات C++، ولكنك تريد التعمق أكثر في التوازي — فهذه المادة مناسبة لك. لن نتعمق في نظرية أنظمة التشغيل، بل سنركز على الأدوات العملية: std::thread، std::mutex، std::async والعمليات الذرية.
1. أساسيات تعدد الخيوط: std::thread وإدارة الخيوط
في C++11 ظهر دعم قياسي للخيوط من خلال الكلاس std::thread. يمكن إنشاء خيط جديد عن طريق تمرير كائن قابل للاستدعاء إلى المُنشئ — دالة، أو لامدا، أو كائن دالة.
#include <iostream>#include <thread>
void hello() { std::cout << "مرحباً من الخيط " << std::this_thread::get_id() << std::endl;}
int main() { std::thread t(hello); // إنشاء الخيط t.join(); // انتظار انتهاء الخيط return 0;}من المهم دائماً استدعاء join() أو detach() لكل خيط تم إنشاؤه، وإلا عند تدمير كائن std::thread سينتهي البرنامج بشكل غير طبيعي. join() يمنع الخيط الحالي حتى يكتمل الخيط الفرعي، بينما detach() يسمح للخيط بالعمل بشكل مستقل.
لتمرير الوسائط إلى الخيط، استخدم معاملات إضافية للمُنشئ:
#include <thread>#include <string>
void print_sum(int a, int b, const std::string& msg) { std::cout << msg << ": " << (a + b) << std::endl;}
int main() { std::thread t(print_sum, 3, 4, "المجموع"); t.join(); return 0;}لاحظ: يتم نسخ الوسائط داخل الخيط. إذا كنت بحاجة لتمرير مرجع، استخدم std::ref().
2. مزامنة الخيوط: الميوتكسات والأقفال
عند العمل مع بيانات مشتركة من خيوط مختلفة، يحدث سباق البيانات (data race). لتجنبه، يتم استخدام المزامنة. الأداة الأساسية هي std::mutex.
#include <iostream>#include <thread>#include <mutex>
std::mutex mtx;int shared_counter = 0;
void increment() { for (int i = 0; i < 100000; ++i) { mtx.lock(); ++shared_counter; mtx.unlock(); }}
int main() { std::thread t1(increment); std::thread t2(increment); t1.join(); t2.join(); std::cout << "النتيجة: " << shared_counter << std::endl; return 0;}لكن الاستدعاء اليدوي لـ lock() و unlock() محفوف بالأخطاء: إذا حدث استثناء بينهما، سيبقى الميوتكس مقفلاً. من الأفضل استخدام غلاف RAII std::lock_guard أو std::unique_lock:
void increment() { for (int i = 0; i < 100000; ++i) { std::lock_guard<std::mutex> lock(mtx); ++shared_counter; } // يتم فتح القفل تلقائياً عند الخروج من النطاق}للتحكم الأكثر مرونة (مثل القفل الشرطي)، استخدم std::unique_lock. وإذا كنت بحاجة لمزامنة عدة ميوتكسات دون حدوث deadlock، استخدم std::lock().
3. المهام غير المتزامنة: std::async و std::future
ليس من المناسب دائماً إدارة الخيوط يدوياً.