std::async، std::future و std::promise
از انتزاعهای سطح بالاتر همروندی استفاده کنید: وظایف ناهمگام را با std::async اجرا کنید، نتایج را با std::future دریافت کنید و مقادیر یا استثناها را بهصورت دستی با std::promise تنظیم کنید.
همروندی مبتنی بر وظیفه
در حالی که std::thread کنترل سطح پایین به شما میدهد، همروندی مبتنی بر وظیفه با std::async و std::future اغلب آسانتر و کمخطاتر است. به جای مدیریت دستی نخها و حالت مشترک، شما کار را به عنوان وظایفی بیان میکنید که نتایج تولید میکنند. زمان اجرا تصمیم میگیرد چگونه آنها را زمانبندی کند.
یک std::future نمایانگر مقداری از نوع T است که در آینده در دسترس خواهد بود. میتوانید get() را فراخوانی کنید تا تا زمان آماده شدن نتیجه مسدود شوید. اگر وظیفه استثنایی پرتاب کرده باشد، get() آن را دوباره پرتاب میکند — که انتشار خودکار استثنا بین نخها را فراهم میکند.
std::async — اجرای وظایف ناهمگام
std::async یک callable را اجرا میکند و یک std::future که نتیجه را نگه میدارد برمیگرداند. میتوانید یک سیاست اجرا مشخص کنید: std::launch::async یک نخ جدید اجباری ایجاد میکند، در حالی که std::launch::deferred اجرا را تا فراخوانی get() به تأخیر میاندازد.
#include <iostream>
#include <future>
#include <chrono>
#include <numeric>
#include <vector>
long long parallel_sum(const std::vector<int>& v, size_t start, size_t end) {
if (end - start < 1000) {
return std::accumulate(v.begin() + start, v.begin() + end, 0LL);
}
size_t mid = start + (end - start) / 2;
// Launch left half asynchronously
auto left = std::async(std::launch::async,
parallel_sum, std::cref(v), start, mid);
// Compute right half in this thread
long long right_sum = parallel_sum(v, mid, end);
return left.get() + right_sum; // .get() blocks until left is done
}
int main() {
std::vector<int> data(1'000'000, 1);
auto start = std::chrono::high_resolution_clock::now();
long long result = parallel_sum(data, 0, data.size());
auto end = std::chrono::high_resolution_clock::now();
auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
std::cout << "Sum: " << result << " in " << ms.count() << "ms\n";
return 0;
}سیاستهای اجرا: async در مقابل deferred
std::launch::async — تابع روی یک نخ جدید اجرا میشود (یا یکی از یک استخر نخ، بسته به پیادهسازی). کار بلافاصله شروع میشود.
std::launch::deferred — تابع تا زمان فراخوانی get() یا wait() روی آینده (future) اجرا نمیشود. در نخ فراخوانکننده اجرا میشود (ارزیابی تنبل). برای محاسباتی که ممکن است به آنها نیاز نداشته باشید مفید است.
پیشفرض (بدون تعیین سیاست) — پیادهسازی انتخاب میکند. این میتواند async، deferred یا هر دو باشد (یعنی در زمان اجرا تصمیم میگیرد). اگر به موازیسازی تضمینشده نیاز دارید، همیشه std::launch::async را مشخص کنید.
std::promise — تنظیم دستی نتیجه
std::promise سمت نوشتن کانال آینده-قول (future-promise) است. شما یک قول (promise) ایجاد میکنید، آینده (future) آن را استخراج میکنید، و سپس از هر نخی set_value() یا set_exception() را فراخوانی میکنید.
#include <iostream>
#include <thread>
#include <future>
#include <stdexcept>
void compute(std::promise<int> prom, int x) {
try {
if (x < 0) {
throw std::invalid_argument("negative input");
}
int result = x * x;
prom.set_value(result); // fulfil the promise
} catch (...) {
prom.set_exception(std::current_exception()); // propagate error
}
}
int main() {
std::promise<int> prom;
std::future<int> fut = prom.get_future(); // get the read end
std::thread t(compute, std::move(prom), 7);
try {
int result = fut.get(); // blocks until value or exception
std::cout << "Result: " << result << "\n"; // prints 49
} catch (const std::exception& e) {
std::cout << "Error: " << e.what() << "\n";
}
t.join();
return 0;
}std::shared_future — مصرفکنندگان متعدد
یک std::future فقط یک بار میتواند get() شود (مقدار را جابجا میکند). std::shared_future به چندین نخ اجازه میدهد منتظر بمانند و همان نتیجه را بخوانند.
#include <iostream>
#include <future>
#include <thread>
#include <vector>
int main() {
auto shared = std::async(std::launch::async, []() {
std::this_thread::sleep_for(std::chrono::seconds(1));
return 42;
}).share(); // convert future to shared_future
// Multiple threads can all read the same result
std::vector<std::thread> consumers;
for (int i = 0; i < 5; ++i) {
consumers.emplace_back([shared, i]() {
int val = shared.get(); // all 5 threads get 42
std::cout << "Consumer " << i << " got: " << val << "\n";
});
}
for (auto& t : consumers) t.join();
return 0;
}یک std::future که توسط std::async برگردانده شده ویژگی خاصی دارد: مخرب (destructor) آن تا تکمیل وظیفه مسدود میشود (مانند یک join ضمنی). این بدان معنی است که اگر آینده برگشتی را رها کنید، فراخوانی «async» عملاً همگام میشود:
// غافلگیری — این مسدود میشود! آینده موقت در
// نقطهویرگول نابود میشود، که منتظر تکمیل وظیفه میماند.
std::async(std::launch::async, long_computation);
// صحیح — آینده را ذخیره کنید تا کار بهطور موازی ادامه یابد
auto fut = std::async(std::launch::async, long_computation);
// ... کار دیگر انجام دهید ...
fut.get(); // حالا منتظر نتیجه بمانیداین رایجترین دام std::async است. همیشه std::future برگشتی را در یک متغیر ذخیره کنید.
std::packaged_task
std::packaged_task یک callable را بستهبندی میکند و یک std::future برای نتیجه آن فراهم میکند. برخلاف std::async، وظیفه را بهطور خودکار اجرا نمیکند — شما آن را بهصورت دستی فراخوانی میکنید (معمولاً با انتقال آن به یک نخ یا صف وظیفه). این آن را ایدهآل برای ساخت استخرهای نخ سفارشی میکند:
1. کار را در یک std::packaged_task بستهبندی کنید.
2. std::future را استخراج کرده و به فراخوانکننده بدهید.
3. وظیفه را به یک صف کاری منتقل کنید.
4. نخهای کارگر وظایف را از صف برداشته و فراخوانی میکنند.
وقتی یک نتیجه از کار همروند میخواهید و نیازی به کنترل دقیق نخ ندارید، از std::async استفاده کنید. این بهطور خودکار استثناها را منتشر و طول عمر نخها را مدیریت میکند. وقتی به کارگران پسزمینه دائمی، زمانبندی سفارشی نیاز دارید، یا زیرساختی مانند استخر نخ میسازید از std::thread (یا std::jthread) استفاده کنید. برای کار «انجامبدهوفراموشکن»، همیشه std::future برگشتی را زنده نگه دارید تا از مسدود شدن تصادفی جلوگیری شود.
std::asyncیک وظیفه را اجرا میکند و یکstd::futureبرمیگرداند — جایگزینی سطح بالا برای مدیریت دستی نخهاstd::launch::asyncیک نخ جدید اجباری ایجاد میکند؛std::launch::deferredاجرا را تا فراخوانیget()به تأخیر میاندازدstd::promiseسمت نوشتن کانال آینده است — برای تنظیم دستی مقدار/استثنا از آن استفاده کنیدstd::shared_futureبه چندین نخ اجازه خواندن همان نتیجه را میدهدstd::futureحاصل ازstd::asyncدر مخرب خود مسدود میشود — همیشه آن را در یک متغیر ذخیره کنیدstd::packaged_taskیک callable را با یک آینده (future) بستهبندی میکند، ایدهآل برای استخرهای نخ سفارشی
آزمون — دانش خود را بسنجید
(15 امتیاز)1. اگر `std::future` برگشتی از `std::async(std::launch::async, fn)` را رها کنید چه اتفاقی میافتد؟
2. تفاوت `std::future` و `std::shared_future` چیست؟
3. `std::async` چگونه استثناها را از وظیفه به فراخوانکننده منتقل میکند؟