std::thread، std::jthread وإدارة المواضيع
تعلم كيفية إنشاء وإدارة سلاسل الرسائل باستخدام std::thread وstd::jthread الخاص بـ C++20. فهم الانضمام مقابل الفصل، والإلغاء التعاوني باستخدام رموز الإيقاف، والتخزين المحلي لسلسلة الرسائل، ومعالجة الاستثناءات عبر حدود سلسلة المحادثات.
لماذا التزامن؟
تحتوي وحدات المعالجة المركزية الحديثة على مراكز متعددة، ولاستغلالها يجب أن يعمل برنامجك بشكل متزامن. قدم C++11 مكتبة ترابط محمولة في، وقام C++20 بتحسينها باستخدامstd::jthread. الخيط هو مسار تنفيذ مستقل يشترك في نفس مساحة العنوان مثل الخيط الذي أنشأه - وهو ما يمثل قوته وخطورته.
على عكس العمليات، تتشارك الخيوط في الذاكرة. وهذا يجعل الاتصال سريعًا ولكنه يقدم أيضًا سباقات البيانات إذا قام خيطان بقراءة وكتابة نفس المتغير دون مزامنة. قبل أن نتناول المزامنة (الدرس التالي)، دعونا نتعلم أولاً كيفية إنشاء سلاسل المحادثات وإدارتها وإغلاقها بشكل واضح.
إنشاء المواضيع مع std::thread
يمكنك إنشاءstd::threadعن طريق تمرير دالة قابلة للاستدعاء (وظيفة، lambda، functor) وأي وسائط. يبدأ تنفيذ الخيط فورًا عند الإنشاء.
#include <iostream>
#include <thread>
void greet(const std::string& name, int times) {
for (int i = 0; i < times; ++i) {
std::cout << "Hello, " << name << "! (" << i + 1 << ")\n";
}
}
int main() {
// Launch a thread running greet("Alice", 3)
std::thread t1(greet, "Alice", 3);
// Lambda-based thread
std::thread t2([]() {
std::cout << "Running in a lambda thread\n";
});
// MUST join or detach before t1/t2 are destroyed
t1.join(); // block until t1 finishes
t2.join();
std::cout << "All threads finished\n";
return 0;
}الانضمام () مقابل الانفصال ()
يجب أن يكون كلstd::threadالذي يمثل مؤشر ترابط قيد التشغيل إما منضمًا أو منفصلًا قبل تشغيل أداة التدمير الخاصة به. إذا نسيت، فسيتم استدعاءstd::terminate()وسيتعطل برنامجك.
يقوم join() بحظر خيط الاستدعاء حتى انتهاء الخيط المستهدف. هذا هو الإعداد الافتراضي الآمن — فأنت تعلم أن سلسلة المحادثات قد انتهت وأن نتائجها جاهزة.
detach() يقطع الاتصال بين كائنstd::threadوخيط نظام التشغيل. يعمل الخيط في الخلفية ("خيط خفي") ولم يعد بإمكانك الانضمام إليه. كن حذرًا: إذا وصل الخيط المنفصل إلى متغيرات المكدس من وظيفة النشر وعادت هذه الوظيفة، فستحصل على سلوك غير محدد.
Neverdetach()a thread that captures or references local variables from the creating scope. When the creating function returns, those locals are destroyed and the detached thread accesses dangling memory. This is undefined behavior and often manifests as sporadic crashes. If you must detach, pass data by value or usestd::shared_ptrto extend lifetimes.
std::jthread - خيط RAII (C++20)
يحلstd::jthreadمشكلتين معstd::thread: (1) ينضم تلقائيًا إلى أداة التدمير الخاصة به، بحيث لا يمكنك نسيانها أبدًا، و(2) يدعم الإلغاء التعاوني عبرstd::stop_token.
#include <iostream>
#include <thread>
#include <chrono>
void worker(std::stop_token stoken, int id) {
while (!stoken.stop_requested()) {
std::cout << "Worker " << id << " running...\n";
std::this_thread::sleep_for(std::chrono::milliseconds(200));
}
std::cout << "Worker " << id << " stopping gracefully\n";
}
int main() {
std::jthread jt1(worker, 1); // stop_token is auto-passed as first arg
std::jthread jt2(worker, 2);
std::this_thread::sleep_for(std::chrono::seconds(1));
// Request both threads to stop
jt1.request_stop();
jt2.request_stop();
// No need to call join() — jthread's destructor does it
std::cout << "Main thread exiting\n";
return 0;
}الاستعلام عن الأجهزة والتخزين المحلي لمؤشر الترابط
تقومstd::thread::hardware_concurrency()بإرجاع عدد مؤشرات الترابط المتزامنة التي يدعمها الجهاز. تمنح مدة تخزينthread_localكل مؤشر ترابط نسخته الخاصة من المتغير.
#include <iostream>
#include <thread>
#include <vector>
thread_local int tls_counter = 0; // each thread gets its own copy
void count_work(int id) {
for (int i = 0; i < 1000; ++i) {
++tls_counter; // no data race — each thread has its own
}
std::cout << "Thread " << id << ": tls_counter = " << tls_counter << "\n";
}
int main() {
unsigned int n = std::thread::hardware_concurrency();
std::cout << "Hardware concurrency: " << n << " threads\n";
std::vector<std::thread> threads;
for (unsigned int i = 0; i < n; ++i) {
threads.emplace_back(count_work, i);
}
for (auto& t : threads) {
t.join();
}
// Each thread printed 1000 — they don't share tls_counter
return 0;
}في C++20 والإصدارات الأحدث، تفضلstd::jthreadعلىstd::thread. فهو ينضم تلقائيًا إلى أداة التدمير الخاصة به، مما يزيل خطر استدعاءstd::terminateإذا نسيت الانضمام. توفر آليةstd::stop_tokenالمدمجة طريقة نظيفة وموحدة لطلب الإلغاء التعاوني - لا حاجة إلى رفع علمatomicالخاص بك.
معالجة الاستثناءات عبر المواضيع
تم طرح استثناء داخل سلسلة رسائل لم يتم اكتشافها داخل سلسلة الرسائل هذه يستدعيstd::terminate(). لا يتم نشر الاستثناءات تلقائيًا إلى سلسلة المحادثات المنضمة.
لنقل الاستثناءات بين سلاسل العمليات، قم بالتقاطها باستخدامstd::current_exception()داخل مؤشر ترابط العامل وتخزينها فيstd::exception_ptr. يمكن بعد ذلك إعادة طرح خيط الانضمام باستخدامstd::rethrow_exception(). تقوم آليةstd::future(الموضحة في الدرس 4) بمعالجة هذا الأمر تلقائيًا.
- يبدأ تنفيذ
std::threadعلى الفور؛ يجب عليكjoin()أوdetach()قبل تشغيل أداة التدمير الخاصة به - ينضم
std::jthread(C++20) تلقائيًا إلى أداة التدمير الخاصة به ويدعم الإلغاء التعاوني عبرstd::stop_token - يمنح
thread_localكل مؤشر ترابط نسخته المستقلة من المتغير - دون الحاجة إلى المزامنة - يخبرك
std::thread::hardware_concurrency()بعدد سلاسل العمليات التي يمكن للجهاز تشغيلها بشكل متزامن - لا تنتشر الاستثناءات الموجودة في سلسلة الرسائل إلى المتصل — استخدم
std::exception_ptrأوstd::futureلنقلها - لا تسمح أبدًا لمؤشر ترابط منفصل بالإشارة إلى المتغيرات المحلية من نطاق النشر
اختبار — اختبر معرفتك
(15 نقطة خبرة)1. ماذا يحدث إذا تم إتلاف كائن`std::thread`الذي يمثل مؤشر ترابط قابل للانضمام دون استدعاء`join()`أو`detach()`؟
2. كيف يدعم`std::jthread`الإلغاء التعاوني؟
3. ماذا تفعل الكلمة الرئيسية`thread_local`؟