C++: مزامنة تعدد الخيوط

آخر تحديث: 2026-08-26

في الدرس 43، تعلمنا أساسيات تعدد الخيوط.

الآن، سنتعمق في مزامنة تعدد الخيوط — جعل عدة خيوط تنسق عملها وتجنب السباقات والجمود المتبادل.

تحدي البرمجة متعددة الخيوط ليس في إنشاء الخيوط — بل في المزامنة.


1. الجمود المتبادل

(1) 1.1 ما هو الجمود المتبادل؟

الجمود المتبادل يحدث عندما ينتظر خيطان أو أكثر بعضهما البعض لتحرير الموارد، فلا يستطيع أي منهما المتابعة.

الشروط الأربعة الضرورية:

  1. الاستبعاد المتبادل: الموارد لا يمكن مشاركتها
  2. الاحتفاظ والانتظار: خيط يحتفظ بموارد وينتظر موارد أخرى
  3. عدم الاستباق: الموارد لا يمكن تحريرها قسرًا
  4. الانتظار الدائري: سلسلة دائرية من الخيوط المنتظرة

(2) 1.2 مثال على الجمود المتبادل

مثال: الجمود المتبادل (الصعوبة ⭐⭐⭐)

▶ مثال 2: عرض البرمجة متعددة الخيوط (الصعوبة ⭐)

TEXT 📖 للعرض فقط
#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;
}

الخرج:

TEXT 📖 للعرض فقط
Thread 1 done
Thread 2 done

💡 نصيحة:


(3) 1.3 تجنب الجمود المتبادل

الطريقة 1: ترتيب قفل ثابت

CPP
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 لقفل عدة كائنات مزامنة في وقت واحد

TEXT 📖 للعرض فقط
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، موصى به)

CPP
void safe_function() {
	std::scoped_lock lock(mtx1, mtx2); // يتجنب الجمود تلقائيًا
	
	// ...
}


2. قفل القراءة-الكتابة

(1) 2.1 لماذا نحتاج أقفال القراءة-الكتابة؟

المشكلة: كائنات المزامنة "متحفظة" جدًا — حتى عندما تقرأ عدة خيوط البيانات فقط، يجب عليها الانتظار.

الحل: قفل القراءة-الكتابة (std::shared_mutex، C++17)

نوع القفل الغرض
قفل حصري عملية كتابة، الخيوط الأخرى لا تستطيع القراءة أو الكتابة
قفل مشترك عملية قراءة، عدة خيوط يمكنها القراءة في وقت واحد

(2) 2.2 مثال: حماية تخزين مؤقت بقفل القراءة-الكتابة (الصعوبة ⭐⭐⭐)

TEXT 📖 للعرض فقط
#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;
}

الخرج:

TEXT 📖 للعرض فقط
(خرج البرنامج)


3. العمليات الذرية

(1) 3.1 ما هي العملية الذرية؟

العملية الذرية هي عملية غير قابلة للتجزئة — إما تُنفذ بالكامل أو لا تُنفذ أبدًا.

الميزة: لا حاجة للقفل، أداء عالٍ.


(2) 3.2 std::atomic

توفر C++11 قالب std::atomic.

مثال: عداد ذري (الصعوبة ⭐⭐)

CPP
#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 يدعم ترتيب الذاكرة للتحكم في نطاق المزامنة.

أوامر الذاكرة الشائعة:



4. تجمع الخيوط

(1) 4.1 لماذا نحتاج تجمع خيوط؟

المشكلة: الإنشاء/التدمير المتكرر للخيوط له عبء عالٍ.

الحل: تجمع الخيوط — إنشاء مسبق لمجموعة من الخيوط وإعادة استخدامها.


(2) 4.2 تنفيذ تجمع خيوط بسيط

مثال: تجمع خيوط (الصعوبة ⭐⭐⭐⭐)

TEXT 📖 للعرض فقط
#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: تسريع الفرز بتعدد الخيوط (الصعوبة ⭐⭐⭐⭐)

CPP
#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;
}
▶ جرّب الكود

الخرج:

TEXT 📖 للعرض فقط
3 1 4 1 5 9 2 6

▶ مثال 3: حماية عداد مشترك بكائن مزامنة (الصعوبة ⭐)

CPP
#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;
}
▶ جرّب الكود

الخرج:

TEXT 📖 للعرض فقط
counter = 10000

❓ أسئلة شائعة

س هل تعدد الخيوط أسرع دائمًا؟
ج ليس بالضرورة. إذا كانت المهام صغيرة جدًا، قد يتجاوز عبء إنشاء الخيوط والمزامنة الفائدة.

س كيف أُصحح أخطاء البرامج متعددة الخيوط؟
ج - استخدم std::cout لطباعة السجلات (لكن أضف كائن مزامنة) - استخدم Valgrind، Helgrind، إلخ للكشف عن سباقات البيانات - استخدم Thread Sanitizer (GCC/Clang)

س ما الجديد في C++20؟
ج - std::jthread (انضمام تلقائي) - الإشارات (std::counting_semaphore) - المزاليج (std::latch) - الحواجز (std::barrier)

📖 ملخص

النقطة الرئيسية الملخص
الجمود المتبادل أربعة شروط، ثلاث طرق للتجنب
قفل القراءة-الكتابة std::shared_mutex (C++17)
العمليات الذرية std::atomic، بدون قفل
تجمع الخيوط إعادة استخدام الخيوط، تقليل العبء
الخوارزميات المتوازية std::execution في C++17

📝 تمارين

  1. أساسي (الصعوبة ⭐): أنشئ خيطين يزيد كل منهما متغيرًا عامًا 100,000 مرة. لاحظ هل النتيجة صحيحة. ثم احمِه بـ std::mutex واختبر مرة أخرى.

  2. متوسط (الصعوبة ⭐⭐): استخدم std::lock_guard لحماية متغير "رصيد الحساب". حاكي خيطين يقومان بعمليات إيداع/سحب في وقت واحد، مع التأكد من أن الرصيد النهائي صحيح.

  3. متقدم (الصعوبة ⭐⭐⭐): استخدم std::condition_variable لتنفيذ نموذج "المنتج-المستهلك". خيط واحد يُنتج بيانات ويضعها في طابور، وخيط آخر يستهلك البيانات. ينتظر المستهلك عندما يكون الطابور فارغًا.



اكتملت المرحلة 6! التالي: المرحلة 7 (متقدم وتطبيق)

Web-Tutorial.com

فريق Web-Tutorial التقني

منصة دروس برمجية يديرها عدة مطورين. كل درس يتم كتابته ومراجعته بواسطة مطورين متخصصين في المجال. نعمل على ضمان دقة وموثوقية المحتوى — إذا لاحظت أي مشكلة، فيرجى إخبارنا.

100%