التزامن وتعدد الخيوط في C++: من الأساسيات إلى التقنيات المتقدمة
تتطلب التطبيقات الحديثة بشكل متزايد أقصى أداء، خاصة عند العمل مع المعالجات متعددة النوى. تقدم لغة C++ أدوات قوية للعمل مع التزامن وتعدد الخيوط، بدءًا من C++11 وحتى أحدث المعايير. في هذه المقالة، سنقوم بتحليل الجوانب المتقدمة لتعدد الخيوط: من الاستخدام الصحيح لكائنات المزامنة والعمليات الذرية إلى أنماط المزامنة المعقدة. المادة موجهة للمطورين الذين هم على دراية بالفعل بأساسيات الخيوط في C++.
1. إدارة الخيوط: std::thread و std::jthread
الفئة std::thread هي أساس البرمجة متعددة الخيوط في C++. ومع ذلك، على المستوى المتقدم، من المهم ليس فقط إنشاء خيط، ولكن أيضًا إدارة دورة حياته بشكل صحيح. مع C++20، ظهر std::jthread، الذي ينضم تلقائيًا عند التدمير، مما يمنع تسرب الموارد.
مثال على استخدام std::jthread:
#include <iostream>#include <thread>#include <chrono>
void worker(int id) { std::this_thread::sleep_for(std::chrono::milliseconds(100 * id)); std::cout << "الخيط " << id << " اكتمل";}
int main() { // std::jthread ينضم تلقائيًا عند الخروج من النطاق std::jthread t1(worker, 1); std::jthread t2(worker, 2); std::jthread t3(worker, 3); // لا حاجة لاستدعاء join() أو detach() بشكل صريح // عند تدمير t1, t2, t3، ينتظر main اكتمالها std::cout << "الخيط الرئيسي يواصل العمل"; return 0;}من المهم أن نتذكر: std::jthread يدعم أيضًا آلية الإيقاف عبر std::stop_token، مما يسمح بإنهاء المهام الخلفية الطويلة بأمان.
2. مزامنة الوصول: كائنات المزامنة، الأقفال، والمتغيرات الشرطية
المشكلة الرئيسية في تعدد الخيوط هي سباقات البيانات. لمنعها، تُستخدم كائنات المزامنة. على المستوى المتقدم، يُوصى باستخدام std::scoped_lock (C++17) لقفل عدة كائنات مزامنة دون خطر الجمود (deadlock).
مثال مع std::scoped_lock:
#include <iostream>#include <mutex>#include <thread>#include <vector>
std::mutex mtx1, mtx2;int shared_data = 0;
void safe_increment(int id) { // scoped_lock يقفل كلا كائني المزامنة بالترتيب، متجنبًا deadlock std::scoped_lock lock(mtx1, mtx2); ++shared_data; std::cout << "الخيط " << id << " زاد القيمة إلى " << shared_data << "";}
int main() { std::vector<std::jthread> threads; for (int i = 0; i < 10; ++i) { threads.emplace_back(safe_increment, i); } return 0;}للسيناريوهات الأكثر تعقيدًا للمزامنة (مثل المنتج-المستهلك)، تُستخدم المتغيرات الشرطية (std::condition_variable). تسمح للخيوط بانتظار حدوث حدث معين دون استهلاك الموارد في الانتظار النشط.
3. العمليات الذرية ونموذج الذاكرة في C++
العمليات الذرية (std::atomic) هي أساس البرمجة الخالية من الأقفال (lock-free programming). تضمن أن قراءة وكتابة المتغير ستتم كعملية واحدة غير قابلة للتجزئة. ومع ذلك، على المستوى المتقدم، من المهم فهم نموذج الذاكرة في C++ ومستويات الترتيب (memory ordering).
المستويات الأساسية لـ memory ordering:
memory_order_relaxed— الحد الأدنى من الضمانات، فقط الذريةmemory_order_acquire— يضمن أن جميع القراءات والكتابات اللاحقة سترى نتائج العمليات السابقةmemory_order_release— يضمن أن جميع العمليات السابقة ستكون مرئية بعد هذه الكتابةmemory_order_acq_rel— مزيج من acquire و release