C++: مزامنة تعدد الخيوط
آخر تحديث: 2026-08-26
في الدرس 43، تعلمنا أساسيات تعدد الخيوط.
الآن، سنتعمق في مزامنة تعدد الخيوط — جعل عدة خيوط تنسق عملها وتجنب السباقات والجمود المتبادل.
تحدي البرمجة متعددة الخيوط ليس في إنشاء الخيوط — بل في المزامنة.
1. الجمود المتبادل
(1) 1.1 ما هو الجمود المتبادل؟
الجمود المتبادل يحدث عندما ينتظر خيطان أو أكثر بعضهما البعض لتحرير الموارد، فلا يستطيع أي منهما المتابعة.
الشروط الأربعة الضرورية:
- الاستبعاد المتبادل: الموارد لا يمكن مشاركتها
- الاحتفاظ والانتظار: خيط يحتفظ بموارد وينتظر موارد أخرى
- عدم الاستباق: الموارد لا يمكن تحريرها قسرًا
- الانتظار الدائري: سلسلة دائرية من الخيوط المنتظرة
(2) 1.2 مثال على الجمود المتبادل
مثال: الجمود المتبادل (الصعوبة ⭐⭐⭐)
▶ مثال 2: عرض البرمجة متعددة الخيوط (الصعوبة ⭐)
#include <iostream>
#include <thread>
#include <mutex>
std::mutex mtx1, mtx2;
void thread1() {
std::lock_guard<std::mutex> lock1(mtx1);
std::this_thread::sleep_for(std::chrono::milliseconds(100));
std::lock_guard<std::mutex> lock2(mtx2); // ينتظر mtx2
std::cout << "Thread 1 done" << std::endl;
}
void thread2() {
std::lock_guardstd::mutex lock2(mtx2);
std::this_thread::sleep_for(std::chrono::milliseconds(100));
std::lock_guardstd::mutex lock1(mtx1); // ينتظر mtx1
std::cout << "Thread 2 done" << std::endl;
}
int main() {
std::thread t1(thread1);
std::thread t2(thread2);
t1.join();
t2.join();
return 0;
}
الخرج:
Thread 1 done
Thread 2 done
💡 نصيحة:
- هذا البرنامج قد لا ينتهي أبدًا (جمود متبادل)
(3) 1.3 تجنب الجمود المتبادل
الطريقة 1: ترتيب قفل ثابت
void thread1() {
std::lock_guardstd::mutex lock1(mtx1);
std::lock_guardstd::mutex lock2(mtx2); // دائمًا mtx1 أولاً، ثم mtx2
std::cout << "Thread 1 done" << std::endl;
}
void thread2() {
std::lock_guardstd::mutex lock1(mtx1); // نفس ترتيب thread1
std::lock_guardstd::mutex lock2(mtx2);
std::cout << "Thread 2 done" << std::endl;
}
الطريقة 2: استخدم std::lock لقفل عدة كائنات مزامنة في وقت واحد
void safe_function() {
std::unique_lock<std::mutex> lock1(mtx1, std::defer_lock);
std::unique_lock<std::mutex> lock2(mtx2, std::defer_lock);
std::lock(lock1, lock2); // قفل متزامن، يتجنب الجمود
// ...
}
الطريقة 3: استخدم std::scoped_lock (C++17، موصى به)
void safe_function() {
std::scoped_lock lock(mtx1, mtx2); // يتجنب الجمود تلقائيًا
// ...
}
2. قفل القراءة-الكتابة
(1) 2.1 لماذا نحتاج أقفال القراءة-الكتابة؟
المشكلة: كائنات المزامنة "متحفظة" جدًا — حتى عندما تقرأ عدة خيوط البيانات فقط، يجب عليها الانتظار.
الحل: قفل القراءة-الكتابة (std::shared_mutex، C++17)
| نوع القفل | الغرض |
|---|---|
| قفل حصري | عملية كتابة، الخيوط الأخرى لا تستطيع القراءة أو الكتابة |
| قفل مشترك | عملية قراءة، عدة خيوط يمكنها القراءة في وقت واحد |
(2) 2.2 مثال: حماية تخزين مؤقت بقفل القراءة-الكتابة (الصعوبة ⭐⭐⭐)
#include <iostream>
#include <thread>
#include <shared_mutex>
#include <unordered_map>
std::unordered_map<int, int> cache;
std::shared_mutex smtx;
int get(int key) {
std::shared_lock<std::shared_mutex> lock(smtx); // قفل مشترك (قراءة)
auto it = cache.find(key);
if (it != cache.end()) {
return it->second;
}
return -1;
}
void set(int key, int value) {
std::unique_lock<std::shared_mutex> lock(smtx); // قفل حصري (كتابة)
cache[key] = value;
}
int main() {
// عدة خيوط يمكنها القراءة في وقت واحد؛ الكتابة حصرية
return 0;
}
الخرج:
(خرج البرنامج)
3. العمليات الذرية
(1) 3.1 ما هي العملية الذرية؟
العملية الذرية هي عملية غير قابلة للتجزئة — إما تُنفذ بالكامل أو لا تُنفذ أبدًا.
الميزة: لا حاجة للقفل، أداء عالٍ.
(2) 3.2 std::atomic
توفر C++11 قالب std::atomic.
مثال: عداد ذري (الصعوبة ⭐⭐)
#include <iostream>
#include <thread>
#include <vector>
#include <atomic>
std::atomic<int> counter(0); // متغير ذري
void increment() {
for (int i = 0; i < 1000; i++) {
counter++; // عملية ذرية، بدون قفل
}
}
int main() {
std::vector<std::thread> threads;
for (int i = 0; i < 10; i++) {
threads.emplace_back(increment);
}
for (auto& t : threads) {
t.join();
}
std::cout << "Counter: " << counter << std::endl; // دائمًا 10000
return 0;
}
(3) 3.3 ترتيب الذاكرة
std::atomic يدعم ترتيب الذاكرة للتحكم في نطاق المزامنة.
أوامر الذاكرة الشائعة:
std::memory_order_relaxed: الأضعف، يضمن الذرية فقطstd::memory_order_acquire: عملية قراءة، يمنع إعادة الترتيبstd::memory_order_release: عملية كتابة، يمنع إعادة الترتيبstd::memory_order_seq_cst: الأقوى، الافتراضي (اتساق متسلسل)
4. تجمع الخيوط
(1) 4.1 لماذا نحتاج تجمع خيوط؟
المشكلة: الإنشاء/التدمير المتكرر للخيوط له عبء عالٍ.
الحل: تجمع الخيوط — إنشاء مسبق لمجموعة من الخيوط وإعادة استخدامها.
(2) 4.2 تنفيذ تجمع خيوط بسيط
مثال: تجمع خيوط (الصعوبة ⭐⭐⭐⭐)
#include <iostream>
#include <thread>
#include <vector>
#include <queue>
#include <functional>
#include <mutex>
#include <condition_variable>
class ThreadPool {
private:
std::vector<std::thread> workers;
std::queue<std::function<void()>> tasks;
std::mutex mtx;
std::condition_variable cv;
bool stop;
public:
ThreadPool(size_t numThreads) : stop(false) {
for (size_t i = 0; i < numThreads; i++) {
workers.emplace_back([this] {
while (true) {
std::function<void()> task;
{
std::unique_lock<std::mutex> lock(mtx);
cv.wait(lock, [this] {
return stop || !tasks.empty();
});
if (stop && tasks.empty()) {
return;
}
task = std::move(tasks.front());
tasks.pop();
}
task();
}
});
}
}
~ThreadPool() {
{
std::lock_guard<std::mutex> lock(mtx);
stop = true;
}
cv.notify_all();
for (auto& t : workers) {
t.join();
}
}
template<class F>
void enqueue(F&& f) {
{
std::lock_guardstd::mutex lock(mtx);
tasks.emplace(std::forward<F>(f));
}
cv.notify_one();
}
};
int main() {
ThreadPool pool(4); // إنشاء 4 خيوط
for (int i = 0; i < 10; i++) {
pool.enqueue([i] {
std::cout << "Task " << i << " running" << std::endl;
});
}
return 0;
}
5. تمرين: الفرز المتوازي
▶ مثال 1: تسريع الفرز بتعدد الخيوط (الصعوبة ⭐⭐⭐⭐)
#include <iostream>
#include <vector>
#include <thread>
#include <algorithm>
void parallelSort(std::vector<int>& v, int left, int right) {
if (left >= right) return;
std::sort(v.begin() + left, v.begin() + right + 1);
}
int main() {
std::vector<int> v = {3, 1, 4, 1, 5, 9, 2, 6};
// فرز متوازي بخيطين
std::thread t1(parallelSort, std::ref(v), 0, v.size()/2);
std::thread t2(parallelSort, std::ref(v), v.size()/2 + 1, v.size()-1);
t1.join();
t2.join();
// دمج النصفين المرتبين
std::inplace_merge(v.begin(), v.begin() + v.size()/2 + 1, v.end());
for (int x : v) {
std::cout << x << " ";
}
std::cout << std::endl;
return 0;
}
الخرج:
3 1 4 1 5 9 2 6
▶ مثال 3: حماية عداد مشترك بكائن مزامنة (الصعوبة ⭐)
#include <iostream>
#include <thread>
#include <vector>
#include <mutex>
int counter = 0;
std::mutex mtx;
void increment() {
for (int i = 0; i < 1000; i++) {
std::lock_guard<std::mutex> lock(mtx);
counter++;
}
}
int main() {
std::vector<std::thread> threads;
for (int i = 0; i < 10; i++) {
threads.emplace_back(increment);
}
for (auto& t : threads) {
t.join();
}
std::cout << "counter = " << counter << std::endl;
return 0;
}
الخرج:
counter = 10000
❓ أسئلة شائعة
std::cout لطباعة السجلات (لكن أضف كائن مزامنة) - استخدم Valgrind، Helgrind، إلخ للكشف عن سباقات البيانات - استخدم Thread Sanitizer (GCC/Clang)std::jthread (انضمام تلقائي) - الإشارات (std::counting_semaphore) - المزاليج (std::latch) - الحواجز (std::barrier)📖 ملخص
| النقطة الرئيسية | الملخص |
|---|---|
| الجمود المتبادل | أربعة شروط، ثلاث طرق للتجنب |
| قفل القراءة-الكتابة | std::shared_mutex (C++17) |
| العمليات الذرية | std::atomic، بدون قفل |
| تجمع الخيوط | إعادة استخدام الخيوط، تقليل العبء |
| الخوارزميات المتوازية | std::execution في C++17 |
📝 تمارين
-
أساسي (الصعوبة ⭐): أنشئ خيطين يزيد كل منهما متغيرًا عامًا 100,000 مرة. لاحظ هل النتيجة صحيحة. ثم احمِه بـ
std::mutexواختبر مرة أخرى. -
متوسط (الصعوبة ⭐⭐): استخدم
std::lock_guardلحماية متغير "رصيد الحساب". حاكي خيطين يقومان بعمليات إيداع/سحب في وقت واحد، مع التأكد من أن الرصيد النهائي صحيح. -
متقدم (الصعوبة ⭐⭐⭐): استخدم
std::condition_variableلتنفيذ نموذج "المنتج-المستهلك". خيط واحد يُنتج بيانات ويضعها في طابور، وخيط آخر يستهلك البيانات. ينتظر المستهلك عندما يكون الطابور فارغًا.
- سباقات الحالة: عدة خيوط تصل إلى البيانات المشتركة في وقت واحد
- std::mutex يحمي الأقسام الحرجة
- std::lock_guard يقفل ويفتح تلقائيًا (RAII)
- الجمود المتبادل: خيطان ينتظران تحرير أقفال بعضهما
- متغير الحالة std::condition_variable يُخطر الخيوط
اكتملت المرحلة 6! التالي: المرحلة 7 (متقدم وتطبيق)