सामग्री पर जाएं

std::async, std::future और std::promise

उच्च-स्तरीय कंकरेंसी एब्स्ट्रैक्शन का उपयोग करें: std::async से असिंक्रोनस टास्क लॉन्च करें, std::future से नतीजे पाएँ, और std::promise से वैल्यू या एक्सेप्शन मैन्युअली सेट करें।

टास्क-आधारित कंकरेंसी

std::thread आपको निम्न-स्तरीय नियंत्रण देता है, जबकि std::async और std::future के साथ टास्क-आधारित कंकरेंसी (task-based concurrency) अक्सर ज़्यादा आसान होती है और उसमें ग़लतियों की गुंजाइश कम रहती है। थ्रेड और साझा स्टेट को मैन्युअली सँभालने की बजाय, आप काम को ऐसे टास्क के रूप में व्यक्त करते हैं जो नतीजे पैदा करते हैं। उन्हें कैसे शेड्यूल करना है, यह रनटाइम तय करता है।

std::future प्रकार T के उस मान को दर्शाता है जो भविष्य में उपलब्ध होगा। नतीजा तैयार होने तक ब्लॉक करने के लिए आप get() कॉल कर सकते हैं। अगर टास्क ने एक्सेप्शन फेंका था, तो get() उसे दोबारा फेंक देता है — इस तरह आपको थ्रेड के पार एक्सेप्शन का स्वचालित प्रसार मिलता है।

std::async — असिंक्रोनस टास्क लॉन्च करना

std::async एक कॉलेबल को लॉन्च करता है और नतीजा रखने वाला std::future लौटाता है। आप एक लॉन्च पॉलिसी (launch policy) बता सकते हैं: 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() कॉल न हो। यह कॉल करने वाले थ्रेड में चलता है (लेज़ी इवैल्युएशन)। उन गणनाओं के लिए उपयोगी जिनकी शायद आपको ज़रूरत न पड़े।

डिफ़ॉल्ट (कोई पॉलिसी न बताने पर) — इम्प्लीमेंटेशन चुनता है। यह 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() केवल एक बार किया जा सकता है (यह वैल्यू को बाहर मूव कर देता है)। 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::async द्वारा लौटाए गए std::future की एक विशेष विशेषता है: उसका डिस्ट्रक्टर टास्क पूरा होने तक ब्लॉक करता है (एक अंतर्निहित join की तरह)। इसका मतलब है कि अगर आप लौटाए गए फ़्यूचर को यूँ ही छोड़ दें, तो "async" कॉल व्यवहार में सिंक्रोनस बन जाती है:

// 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

std::async के साथ यह सबसे आम गड़बड़ी है। लौटाए गए std::future को हमेशा किसी वेरिएबल में सँभालकर रखें।

std::packaged_task

std::packaged_task एक कॉलेबल को रैप करता है और उसके नतीजे के लिए एक std::future देता है। std::async के विपरीत, यह टास्क को अपने-आप नहीं चलाता — आप उसे मैन्युअली इनवोक करते हैं (आमतौर पर उसे किसी थ्रेड या टास्क क्यू में मूव करके)। इस वजह से यह कस्टम थ्रेड पूल बनाने के लिए आदर्श है:

1. काम को std::packaged_task में रैप करें।
2. std::future निकालें और उसे कॉलर को दे दें।
3. टास्क को वर्क क्यू में मूव करें।
4. वर्कर थ्रेड क्यू से टास्क निकालकर उन्हें इनवोक करते हैं।

सर्वोत्तम अभ्यास

जब आपको कंकरेंट काम से नतीजा चाहिए और बारीक थ्रेड नियंत्रण की ज़रूरत नहीं, तब std::async का उपयोग करें। यह एक्सेप्शन को अपने-आप आगे पहुँचाता है और थ्रेड के लाइफ़टाइम को सँभालता है। जब आपको स्थायी बैकग्राउंड वर्कर, कस्टम शेड्यूलिंग चाहिए, या आप थ्रेड पूल जैसा इन्फ्रास्ट्रक्चर बना रहे हों, तब std::thread (या std::jthread) का उपयोग करें। फ़ायर-एंड-फ़ॉरगेट (fire-and-forget) काम के लिए, अनजाने में ब्लॉकिंग से बचने हेतु लौटाए गए std::future को हमेशा जीवित रखें।

मुख्य बातें
  • std::async एक टास्क लॉन्च करता है और std::future लौटाता है — मैन्युअल थ्रेड मैनेजमेंट का एक उच्च-स्तरीय विकल्प
  • std::launch::async नया थ्रेड बनाने के लिए मजबूर करता है; std::launch::deferred get() कॉल होने तक देरी करता है
  • std::promise फ़्यूचर चैनल का लिखने वाला सिरा है — वैल्यू/एक्सेप्शन मैन्युअली सेट करने के लिए इसका उपयोग करें
  • std::shared_future कई थ्रेड को एक ही नतीजा पढ़ने देता है
  • std::async से मिला std::future अपने डिस्ट्रक्टर में ब्लॉक करता है — इसे हमेशा किसी वेरिएबल में सँभालकर रखें
  • std::packaged_task एक कॉलेबल को फ़्यूचर के साथ रैप करता है, कस्टम थ्रेड पूल के लिए आदर्श

क्विज़ — अपना ज्ञान परखें

(15 XP)

1. अगर आप `std::async(std::launch::async, fn)` द्वारा लौटाए गए `std::future` को यूँ ही छोड़ दें, तो क्या होता है?

2. `std::future` और `std::shared_future` में क्या अंतर है?

3. `std::async` टास्क से कॉलर तक एक्सेप्शन कैसे पहुँचाता है?