std::thread، std::jthread و مدیریت نخها
ایجاد و مدیریت نخها با std::thread و std::jthread در C++20 را بیاموزید. مفاهیم join در مقابل detach، لغو مشارکتی با stop tokenها، ذخیرهسازی محلی نخ و مدیریت استثناها در مرزهای نخها را درک کنید.
چرا همروندی؟
پردازندههای مدرن دارای هستههای متعدد هستند و برای بهرهبرداری از آنها، برنامه شما باید کار را به صورت همروند اجرا کند. C++11 یک کتابخانه قابل حمل نخها را در معرفی کرد و C++20 آن را با std::jthread بهبود داد. یک نخ (thread) مسیر اجرای مستقلی است که همان فضای آدرس نخ ایجادکننده خود را به اشتراک میگذارد — که هم قدرت و هم خطر آن است.
برخلاف فرآیندها، نخها حافظه را به اشتراک میگذارند. این ارتباط را سریع میکند اما اگر دو نخ بدون همزمانسازی، همان متغیر را بخوانند و بنویسند، مسابقه داده (data race) ایجاد میشود. قبل از پرداختن به همزمانسازی (درس بعدی)، ابتدا بیاموزیم چگونه نخها را ایجاد، مدیریت و بهدرستی متوقف کنیم.
ایجاد نخها با std::thread
شما یک std::thread را با ارسال یک callable (تابع، لامبدا، 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;
}join() در مقابل detach()
هر std::thread که نماینده یک نخ در حال اجرا باشد، باید قبل از اجرای مخرب (destructor) آن، یا join شود یا detach. اگر فراموش کنید، std::terminate() فراخوانی میشود و برنامهتان متوقف میشود.
join() نخ فراخوان را مسدود میکند تا نخ هدف تمام شود. این حالت پیشفرض امن است — شما مطمئنید که نخ تمام شده و نتایج آن آماده است.
detach() اتصال بین شیء std::thread و نخ سیستمعامل را قطع میکند. نخ در پسزمینه اجرا میشود (یک «نخ شبح» یا daemon thread) و دیگر نمیتوانید به آن join کنید. احتیاط کنید: اگر نخ detach شده به متغیرهای پشته از تابع ایجادکننده دسترسی داشته باشد و آن تابع بازگشت کند، رفتار نامعین (undefined behavior) رخ میدهد.
هرگز یک نخ را که متغیرهای محلی از محدوده ایجادکننده را capture یا ارجاع میدهد، detach() نکنید. وقتی تابع ایجادکننده بازمیگردد، آن متغیرهای محلی نابود میشوند و نخ detach شده به حافظه معلق دسترسی پیدا میکند. این رفتار نامعین (undefined behavior) است و اغلب به صورت خرابیهای پراکنده ظاهر میشود. اگر مجبور به detach هستید، دادهها را با مقدار (by value) ارسال کنید یا از std::shared_ptr برای افزایش طول عمر استفاده کنید.
std::jthread — نخ RAII (سیپلاسپلاس ۲۰)
std::jthread دو مشکل std::thread را حل میکند: (۱) بهطور خودکار در مخرب (destructor) خود join میکند، بنابراین هرگز فراموش نمیکنید، و (۲) از لغو مشارکتی از طریق 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 ترجیح دهید. این بهطور خودکار در مخرب (destructor) خود join میکند و خطر فراخوانی std::terminate را در صورت فراموش کردن join حذف میکند. مکانیزم داخلی std::stop_token آن یک روش تمیز و استاندارد برای درخواست لغو مشارکتی فراهم میکند — نیازی به ساخت پرچم atomic خودتان نیست.
مدیریت استثنا در بین نخها
استثنایی که درون یک نخ پرتاب شود و در همان نخ گرفته نشود، باعث فراخوانی std::terminate() میشود. استثناها بهطور خودکار به نخ joinکننده منتقل نمیشوند.
برای انتقال استثناها بین نخها، آنها را با std::current_exception() در نخ کارگر capture کنید و در یک std::exception_ptr ذخیره کنید. نخ joinکننده سپس میتواند با std::rethrow_exception() آن را دوباره پرتاب کند. مکانیزم std::future (که در درس ۴ پوشش داده میشود) این کار را بهطور خودکار انجام میدهد.
std::threadبلافاصله شروع به اجرا میکند؛ باید قبل از اجرای مخرب آن،join()یاdetach()کنیدstd::jthread(سیپلاسپلاس ۲۰) بهطور خودکار در مخرب خود join میکند و از لغو مشارکتی از طریقstd::stop_tokenپشتیبانی میکندthread_localبه هر نخ کپی مستقل خودش از یک متغیر را میدهد — نیازی به همزمانسازی نیستstd::thread::hardware_concurrency()تعداد نخهایی که سختافزار میتواند بهطور همزمان اجرا کند را نشان میدهد- استثناها در یک نخ به فراخوانکننده منتقل نمیشوند — از
std::exception_ptrیاstd::futureبرای انتقال آنها استفاده کنید - هرگز اجازه ندهید یک نخ detach شده به متغیرهای محلی از محدوده ایجادکننده ارجاع دهد
آزمون — دانش خود را بسنجید
(15 امتیاز)1. اگر یک شیء `std::thread` که نماینده یک نخ joinable است، بدون فراخوانی `join()` یا `detach()` نابود شود، چه اتفاقی میافتد؟
2. `std::jthread` چگونه از لغو مشارکتی پشتیبانی میکند؟
3. کلمه کلیدی `thread_local` چه کاری انجام میدهد؟