رفتن به محتوا

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() به تأخیر می‌اندازد.

async_parallel_sum.cpp
#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() را فراخوانی می‌کنید.

promise_demo.cpp
#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 به چندین نخ اجازه می‌دهد منتظر بمانند و همان نتیجه را بخوانند.

shared_future.cpp
#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` چگونه استثناها را از وظیفه به فراخوان‌کننده منتقل می‌کند؟