Μετάβαση στο περιεχόμενο

std::async, std::future & std::promise

Χρησιμοποιήστε αφαιρέσεις συγχρονισμού υψηλότερου επιπέδου: εκκινήστε ασύγχρονες εργασίες με το std::async, ανακτήστε αποτελέσματα με το std::future και ορίστε μη αυτόματα τιμές ή εξαιρέσεις με το std::promise.

Task-Based Concurrency

Ενώ τοstd::threadσάς παρέχει έλεγχο χαμηλού επιπέδου, η συμφωνία βάσει εργασιών με ταstd::asyncκαιstd::futureείναι συχνά ευκολότερη και λιγότερο επιρρεπής σε σφάλματα. Αντί να διαχειρίζεστε με μη αυτόματο τρόπο τα νήματα και την κοινή κατάσταση, εκφράζετε την εργασία ως εργασίες που παράγουν αποτελέσματα. Ο χρόνος εκτέλεσης αποφασίζει πώς θα τα προγραμματίσετε.

Έναstd::futureαντιπροσωπεύει μια τιμή τύπουTπου θα είναι διαθέσιμη στο μέλλον. Μπορείτε να καλέσετε τοget()για αποκλεισμό μέχρι να είναι έτοιμο το αποτέλεσμα. Εάν η εργασία έθεσε μια εξαίρεση, τοget()την επαναφέρει — δίνοντάς σας αυτόματη διάδοση εξαιρέσεων σε νήματα.

std::async — Launching Asynchronous Tasks

std::asynclaunches a callable and returns astd::futureholding the result. You can specify a launch policy:std::launch::asyncforces a new thread, whilestd::launch::deferreddelays execution untilget()is called.

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;
}

Launch Policies: async vs 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::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::μέλλον.

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) όταν χρειάζεστε επίμονους εργάτες στο παρασκήνιο, προσαρμοσμένο προγραμματισμό ή όταν δημιουργείτε υποδομές όπως ομάδες νημάτων. Για να βάλετε φωτιά και να ξεχάσετε την εργασία, κρατήστε πάντα ζωντανό το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τυλίγει ένα καλούμενο με ένα μέλλον, ιδανικό για προσαρμοσμένες ομάδες νημάτων

Κουίζ — Δοκιμάστε τις γνώσεις σας

(15 XP)

1. Τι θα συμβεί αν απορρίψετε το`std::future`που επιστράφηκε από το`std::async(std::launch::async, fn)`;

2. What is the difference between`std::future`and`std::shared_future`?

3. How does`std::async`propagate exceptions from the task to the caller?