C++: أساسيات تعدد الخيوط
آخر تحديث: 2026-08-26
في الدرس 42، تعلمنا عن التعابير النمطية.
الآن، سندخل المنطقة المتقدمة من C++ — البرمجة متعددة الخيوط.
الحواسيب الحديثة كلها متعددة النوى، لكن البرامج أحادية الخيط تستخدم نواة واحدة فقط — ما أضيع ذلك.
تعدد الخيوط يتيح لك القيام بعدة أشياء في وقت واحد، مما يحسن الأداء بشكل كبير.
1. نظرة عامة على تعدد الخيوط
(1) 1.1 ما هو الخيط؟
الخيط هو أصغر وحدة تنفيذ للبرنامج.
العملية مقابل الخيط:
- العملية: وحدة تخصيص الموارد (مساحة ذاكرة مستقلة)
- الخيط: وحدة التنفيذ (يشارك ذاكرة العملية)
تشبيه من الواقع:
- العملية = المصنع
- الخيط = العامل (عدة عمال يشاركون موارد المصنع)
(2) 1.2 لماذا نستخدم تعدد الخيوط؟
| الميزة | الوصف |
|---|---|
| تحسين الأداء | حوسبة متوازية متعددة النوى |
| استجابة أفضل | خيط واجهة المستخدم لا يحجب |
| تبسيط التصميم | تعيين مهام مختلفة لخيوط مختلفة |
2. إنشاء الخيوط
(1) 2.1 الاستخدام الأساسي
توفر C++11 صنف std::thread في ملف الرأس thread.
مثال: إنشاء خيط (الصعوبة ⭐)
▶ مثال 1: عرض البرمجة متعددة الخيوط (الصعوبة ⭐)
#include <iostream>
#include <thread>
void hello() {
std::cout << "Hello from thread!" << std::endl;
}
int main() {
std::thread t(hello); // إنشاء خيط
t.join(); // انتظار انتهاء الخيط
std::cout << "Main thread ends" << std::endl;
return 0;
}
الخرج:
Main thread: starting
Main thread: waiting for child thread
Child thread: Hello from thread
Main thread: ended
نتيجة التشغيل:
Hello from thread!
Main thread ends
(2) 2.2 join مقابل detach
| الدالة | الغرض | الوصف |
|---|---|---|
join() |
انتظار انتهاء الخيط | تحجب الخيط الحالي |
detach() |
فصل الخيط | الخيط يعمل بشكل مستقل، لا يمكن ربطه بعد الآن |
مثال: الانتظار بـ join (الصعوبة ⭐)
#include <iostream>
### ▶ مثال 2: عرض البرمجة متعددة الخيوط (الصعوبة ⭐)
#include <thread>
#include <chrono>
void worker(int id) {
for (int i = 0; i < 3; i++) {
std::cout << "Worker " << id << " working..." << std::endl;
std::this_thread::sleep_for(std::chrono::milliseconds(500));
}
}
int main() {
std::thread t1(worker, 1);
std::thread t2(worker, 2);
t1.join(); // انتظار انتهاء t1
t2.join(); // انتظار انتهاء t2
std::cout << "All workers done" << std::endl;
return 0;
}
3. تمرير المعاملات إلى الخيوط
(1) 3.1 تمرير المعاملات
مُنشئ std::thread يمكنه قبول أي كائن قابل للاستدعاء ومعاملات.
مثال: تمرير معاملات (الصعوبة ⭐⭐)
#include <iostream>
#include <thread>
#include <string>
void printMessage(std::string msg, int count) {
for (int i = 0; i < count; i++) {
std::cout << msg << std::endl;
}
}
int main() {
std::thread t(printMessage, "Hello", 3);
t.join();
return 0;
}
(2) 3.2 التمرير بالمرجع
افتراضيًا، تُمرر المعاملات بالقيمة. للتمرير بالمرجع، يجب استخدام std::ref.
مثال: التمرير بالمرجع (الصعوبة ⭐⭐)
#include <iostream>
#include <thread>
#include <functional>
void increment(int& x) {
x++;
}
int main() {
int counter = 0;
std::thread t(increment, std::ref(counter));
t.join();
std::cout << "Counter: " << counter << std::endl; // الخرج: 1
return 0;
}
4. كائنات المزامنة
(1) 4.1 لماذا نحتاج كائنات المزامنة؟
المشكلة: وصول عدة خيوط إلى بيانات مشتركة في وقت واحد يسبب سباقات البيانات.
مثال: سباق بيانات (الصعوبة ⭐⭐)
#include <iostream>
#include <thread>
#include <vector>
int counter = 0;
void increment() {
for (int i = 0; i < 1000; i++) {
counter++; // عدة خيوط تعدّل في وقت واحد، النتيجة غير مؤكدة
}
}
int main() {
std::vectorstd::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;
}
(2) 4.2 حماية البيانات المشتركة بكائن مزامنة
كائن المزامنة (Mutex) يضمن أن خيط واحد فقط يصل إلى البيانات المشتركة في كل مرة.
مثال: الحماية بكائن مزامنة (الصعوبة ⭐⭐)
#include <iostream>
#include <thread>
#include <vector>
#include <mutex>
int counter = 0;
std::mutex mtx;
void increment() {
for (int i = 0; i < 1000; i++) {
mtx.lock(); // القفل
counter++;
mtx.unlock(); // فتح القفل
}
}
int main() {
std::vectorstd::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) 4.3 lock_guard — أسلوب RAII
موصى به: استخدم std::lock_guard للقفل/فتح القفل تلقائيًا.
void increment() {
for (int i = 0; i < 1000; i++) {
std::lock_guardstd::mutex lock(mtx); // يقفل عند الإنشاء، يفتح عند التدمير
counter++;
} // فتح تلقائي
}
5. متغيرات الحالة
(1) 5.1 لماذا نحتاج متغيرات الحالة؟
المشكلة: خيط يحتاج انتظار شرط ليصبح صحيحًا (مثلاً: الطابور ليس فارغًا).
الحل: std::condition_variable
(2) 5.2 مثال: المنتج-المستهلك (الصعوبة ⭐⭐⭐)
#include <iostream>
#include <thread>
#include <queue>
#include <mutex>
#include <condition_variable>
std::queueint q;
std::mutex mtx;
std::condition_variable cv;
void producer() {
for (int i = 0; i < 10; i++) {
std::lock_guardstd::mutex lock(mtx);
q.push(i);
std::cout << "Produced: " << i << std::endl;
cv.notify_one(); // إخطار المستهلك
}
}
void consumer() {
for (int i = 0; i < 10; i++) {
std::unique_lockstd::mutex lock(mtx);
cv.wait(lock, { return !q.empty(); }); // انتظر حتى لا يكون الطابور فارغًا
int value = q.front();
q.pop();
std::cout << "Consumed: " << value << std::endl;
}
}
int main() {
std::thread t1(producer);
std::thread t2(consumer);
t1.join();
t2.join();
return 0;
}
الخرج:
Produced:
Consumed:
6. المهام غير المتزامنة
(1) 6.1 std::async
std::async يُستخدم لإطلاق مهمة غير متزامنة ويعيد std::future.
مثال: حساب غير متزامن (الصعوبة ⭐⭐)
#include <iostream>
#include <future>
int calculate(int x) {
return x * x;
}
int main() {
std::futureint result = std::async(calculate, 10);
std::cout << "Result: " << result.get() << std::endl; // الخرج: 100
return 0;
}
❓ أسئلة شائعة
س: كم خيط يجب أن أستخدم؟ ج: عادةً يساوي عدد نوى وحدة المعالجة المركزية. الخيوط الكثيرة جدًا تسبب عبء تبديل السياق.
س: ما هو الجمود المتبادل؟ ج: خيطان ينتظران تحرير أقفال بعضهما البعض، فلا يستطيع أي منهما المتابعة.
كيفية التجنب:
- اقفل بترتيب ثابت
- استخدم
std::lock()لقفل عدة كائنات مزامنة في وقت واحد - استخدم
std::scoped_lock(C++17)
std::thread: مرن، متعدد المنصات - OpenMP: بسيط، جيد للحوسبة العلمية▶ مثال 3: إنشاء خيوط (الصعوبة ⭐)
#include <iostream>
#include <thread>
void printNumbers(int start, int end) {
for (int i = start; i <= end; i++) {
std::cout << i << " ";
}
std::cout << std::endl;
}
int main() {
std::thread t1(printNumbers, 1, 5);
std::thread t2(printNumbers, 10, 15);
t1.join();
t2.join();
return 0;
}
الخرج:
مثال على الخرج:
1 10 2 11 3 12 4 13 5 14 15
std::thread يُنشئ خيوط، join() ينتظر انتهاءها. قد يختلف ترتيب الخرج بسبب جدولة الخيوط.
| النقطة الرئيسية | الملخص |
|---|---|
| std::thread | إنشاء خيوط |
| join/detach | انتظار/فصل الخيوط |
| std::mutex | كائن مزامنة، يحمي البيانات المشتركة |
| std::lock_guard | قفل بأسلوب RAII |
| std::condition_variable | متغير حالة، تواصل بين الخيوط |
| std::async | مهام غير متزامنة |
📖 ملخص
- std::thread: إنشاء خيوط
- join(): انتظار انتهاء الخيط
- detach(): فصل الخيط
- دوال الخيوط: يمكن تمرير مؤشرات دوال، أو تعابير لامدا، أو كائنات دوال
📝 تمارين
-
أساسي (الصعوبة ⭐): أنشئ خيطين يخرجان "Thread A" و"Thread B" على التوالي، ولاحظ عشوائية ترتيب الخرج.
-
متوسط (الصعوبة ⭐⭐): أنشئ 4 خيوط، كل منها يحسب مجموعًا تراكميًا لمدى من الأرقام (مثلاً 1-2500، 2501-5000...)، ثم اجمع النتائج في النهاية.
-
متقدم (الصعوبة ⭐⭐⭐): استخدم
std::asyncوstd::futureلتنفيذ محاكي تحميل متزامن: أنشئ 3 مهام غير متزامنة، كل منها تحاكي تحميل ملف بحجم مختلف، وانتظر اكتمالها جميعًا قبل التلخيص.
- std::thread يُنشئ خيوط بتمرير كائنات قابلة للاستدعاء
- join ينتظر انتهاء الخيط، detach يفصل الخيط
- المتغيرات العامة المشتركة بين الخيوط تحتاج مزامنة
- std::this_thread::sleep_for يُنيم الخيوط
- عدد الخيوط يجب ألا يتجاوز دعم الأجهزة (hardware_concurrency)
الدرس التالي: مزامنة تعدد الخيوط (#44)