انتقل إلى المحتوى

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عنصرًا قابلاً للاستدعاء ويعيد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;
}

سياسات الإطلاق: غير متزامن مقابل مؤجل

std::launch::async — يتم تنفيذ الوظيفة على مؤشر ترابط جديد (أو واحد من تجمع مؤشرات الترابط، محدد التنفيذ). يبدأ العمل على الفور.

std::launch::deferred — لا يتم تنفيذ الوظيفة حتى يتم استدعاءget()أوwait()في المستقبل. يتم تشغيله في مؤشر ترابط الاستدعاء (تقييم كسول). مفيدة للحسابات التي قد لا تحتاج إليها.

الافتراضي (لم يتم تحديد سياسة) — يتم اختيار التنفيذ. يمكن أن يكون هذاasyncأوdeferredأو كليهما (بمعنى أنه يقرر في وقت التشغيل). إذا كنت بحاجة إلى توازي مضمون، فحدد دائمًاstd::launch::async.

std::promise — إعداد النتائج يدويًا

std::promiseهي نهاية الكتابة لقناة الوعد المستقبلي. يمكنك إنشاء وعد، واستخراج مستقبله، ثم إما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()-ed مرة واحدة فقط (ينقل القيمة للخارج). يسمح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على خاصية خاصة: أداة التدمير الخاصة به الكتل حتى تكتمل المهمة (مثلjoinالضمني). وهذا يعني أنه إذا قمت بتجاهل المستقبل الذي تم إرجاعه، فإن استدعاء "غير المتزامن" يصبح متزامنًا بشكل فعال:

// SURPRISE — this blocks! The temporary future is destroyed
// at the semicolon, which waits for the task to finish.
std::async(std::launch::async, long_computation);

// CORRECT — store the future to let work proceed in parallel
auto fut = std::async(std::launch::async, long_computation);
// ... do other work ...
fut.get();  // now wait for the result

This is the most common pitfall with std::async. Always capture the returned std::future.

std::package_task

يغلفstd::packaged_taskعنصرًا قابلاً للاستدعاء ويوفر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_taskwraps a callable with a future, ideal for custom thread pools

اختبار — اختبر معرفتك

(15 نقطة خبرة)

1. What happens if you discard the`std::future`returned by`std::async(std::launch::async, fn)`?

2. ما الفرق بين`std::future`و`std::shared_future`؟

3. كيف يقوم`std::async`بنشر الاستثناءات من المهمة إلى المتصل؟