C++20 Coroutines
Understand coroutines as suspendable functions. Learn co_yield, co_return, co_await, the generator pattern, and practical use cases like async I/O and state machines.
What Are Coroutines?
Μια κορουτίνα είναι μια συνάρτηση που μπορεί να αναστείλει την εκτέλεσή της και να συνεχιστεί αργότερα. Σε αντίθεση με τις κανονικές συναρτήσεις που εκτελούνται από την αρχή μέχρι το τέλος, οι κορουτίνες μπορούν να κάνουν παύση στη μέση της εκτέλεσης, να αποδώσουν ενδιάμεσα αποτελέσματα και να συνεχίσουν από εκεί που σταμάτησαν. Αυτό είναι θεμελιωδώς διαφορετικό από τα νήματα: οι κορουτίνες είναι συνεργάσιμες (επιλέγουν πότε να αναστείλουν), ελαφριές (δεν απαιτείται νήμα λειτουργικού συστήματος) και ντετερμινιστικές (χωρίς αγώνες δεδομένων από ταυτόχρονη πρόσβαση).
Το C++20 εισάγει την υποστήριξη κορουτίνας σε επίπεδο γλώσσας με τρεις νέες λέξεις-κλειδιά: co_yield (αναστολή και παραγωγή τιμής), co_return (συμπλήρωση της κορουτίνας με μια τελική τιμή) και co_await (αναστολή έως ότου ολοκληρωθεί μια λειτουργία ασυγχρονισμού). Οποιαδήποτε συνάρτηση περιέχει μία από αυτές τις λέξεις-κλειδιά είναι αυτόματα κορουτίνα.
The Generator Pattern
Μια γεννήτρια είναι μια κορουτίνα που παράγει μια ακολουθία τιμών νωχελικά. Κάθε κλήση προς τοco_yieldαναστέλλει την κορουτίνα και παραδίδει μια τιμή στον καλούντα. Ακολουθεί μια πλήρης, μεταγλωττιζόμενη εφαρμογή γεννήτριας:
#include <iostream>
#include <coroutine>
#include <optional>
// A simple Generator type
template<typename T>
struct Generator {
struct promise_type {
T current_value;
Generator get_return_object() {
return Generator{std::coroutine_handle<promise_type>::from_promise(*this)};
}
std::suspend_always initial_suspend() { return {}; }
std::suspend_always final_suspend() noexcept { return {}; }
std::suspend_always yield_value(T value) {
current_value = std::move(value);
return {};
}
void return_void() {}
void unhandled_exception() { std::terminate(); }
};
std::coroutine_handle<promise_type> handle;
explicit Generator(std::coroutine_handle<promise_type> h) : handle(h) {}
~Generator() { if (handle) handle.destroy(); }
// Move only
Generator(Generator&& other) noexcept : handle(other.handle) { other.handle = nullptr; }
Generator& operator=(Generator&&) = delete;
Generator(const Generator&) = delete;
bool next() {
handle.resume();
return !handle.done();
}
T value() const { return handle.promise().current_value; }
};
// A coroutine that generates Fibonacci numbers
Generator<long long> fibonacci() {
long long a = 0, b = 1;
while (true) {
co_yield a;
auto next = a + b;
a = b;
b = next;
}
}
// A coroutine that generates a range
Generator<int> range(int start, int end) {
for (int i = start; i < end; ++i) {
co_yield i;
}
}
int main() {
auto fib = fibonacci();
std::cout << "First 10 Fibonacci numbers: ";
for (int i = 0; i < 10 && fib.next(); ++i) {
std::cout << fib.value() << " ";
}
std::cout << "\n"; // 0 1 1 2 3 5 8 13 21 34
auto r = range(1, 6);
while (r.next()) {
std::cout << r.value() << " "; // 1 2 3 4 5
}
std::cout << "\n";
return 0;
}Coroutine Machinery: Promise & Handle
Κάθε κορουτίνα έχει δύο βασικά στοιχεία:
promise_type — Ελέγχει τη συμπεριφορά της κορουτίνας. Ο μεταγλωττιστής αναζητά ένα ένθετοpromise_typeστον τύπο επιστροφής της κορουτίνας. Βασικές μέθοδοι:
-get_return_object()— δημιουργεί το αντικείμενο που επιστρέφεται στον καλούντα
-initial_suspend()— επιστρέφειsuspend_always(τεμπέλης εκκίνηση) ήsuspend_never(ανυπόμονη έναρξη)
-final_suspend()— τι να κάνετε όταν τελειώσει η κορουτίνα (πρέπει να είναιnoexcept)
-yield_value(T)— κλήθηκε στοco_yield
-return_value(T)ήreturn_void()— καλείται στοco_return
-unhandled_exception()— καλείται εάν διαφύγει μια εξαίρεση
std::coroutine_handle — Ένας δείκτης με διαγραφή τύπου προς το πλαίσιο κορουτίνας. Παρέχει μεθόδουςresume(),done()καιdestroy(). Το πλαίσιο κορουτίνας ζει στο σωρό και περιέχει όλες τις τοπικές μεταβλητές και το σημείο ανάρτησης.
Η ροή είναι: ο καλών δημιουργεί κορουτίνα ->initial_suspend-> ο καλών καλεί τοresume()-> η κορουτίνα εκτελείται έως ότου τοco_yield/co_return/co_await-> ο έλεγχος επιστρέψει στον καλούντα -> επανάληψη.
co_await και Awaitable Objects
Τοco_awaitαναστέλλει μια κορουτίνα μέχρι να ολοκληρωθεί μια ασύγχρονη λειτουργία. Οποιοσδήποτε τύπος που υλοποιεί τη διεπαφή Αναμενόμενη μπορεί να χρησιμοποιηθεί με τοco_await. Αυτή είναι η βάση για ασύγχρονα πλαίσια εισόδου/εξόδου:
#include <iostream>
#include <coroutine>
#include <chrono>
#include <thread>
// A simple awaitable that simulates async work
struct AsyncTimer {
std::chrono::milliseconds duration;
bool await_ready() const noexcept {
return duration.count() <= 0; // skip suspend if no wait needed
}
void await_suspend(std::coroutine_handle<> handle) const {
// In production, this would register with an event loop
// Here we simulate with a thread
std::thread([handle, d = duration]() {
std::this_thread::sleep_for(d);
handle.resume(); // resume the coroutine
}).detach();
}
void await_resume() const noexcept {
// Called when the coroutine resumes — return value becomes
// the result of the co_await expression
}
};
// The three Awaitable methods:
// 1. await_ready() — can we skip suspending? (optimization)
// 2. await_suspend() — what to do when suspending
// 3. await_resume() — what value to produce when resuming
// A Task type for async coroutines
struct Task {
struct promise_type {
Task get_return_object() { return {}; }
std::suspend_never initial_suspend() { return {}; }
std::suspend_never final_suspend() noexcept { return {}; }
void return_void() {}
void unhandled_exception() { std::terminate(); }
};
};
Task async_work() {
std::cout << "Starting async work...\n";
co_await AsyncTimer{std::chrono::milliseconds(100)};
std::cout << "Phase 1 complete\n";
co_await AsyncTimer{std::chrono::milliseconds(100)};
std::cout << "Phase 2 complete\n";
}
int main() {
async_work();
std::this_thread::sleep_for(std::chrono::milliseconds(500));
return 0;
}Πρακτικές περιπτώσεις χρήσης
Οι κορουτίνες υπερέχουν σε πολλά σενάρια πραγματικού κόσμου:
Γεννήτριες / Lazy Sequences — Παράγετε τιμές κατ' απαίτηση χωρίς να υπολογίζετε ολόκληρη την ακολουθία εκ των προτέρων. Ιδανικό για άπειρες ακολουθίες, ανάλυση αρχείων και αγωγούς δεδομένων.
Async I/O — Οι διακομιστές δικτύου, τα ερωτήματα της βάσης δεδομένων και οι λειτουργίες αρχείων μπορούν να ολοκληρώσουν τοco_awaitχωρίς να μπλοκάρουν νήματα. Βιβλιοθήκες όπως οιcppcoro,libunifexκαι Boost.Asio παρέχουν πρωτόγονα I/O έτοιμα για κορουτίνα.
State Machines — Κάθεco_yieldήco_awaitαντιπροσωπεύει μια μετάβαση κατάστασης. Οι τοπικές μεταβλητές της κορουτίνας διατηρούν φυσικά την κατάσταση μεταξύ των μεταβάσεων, εξαλείφοντας τις μη αυτόματες δομές κατάστασης.
Προγραμματισμός βάσει συμβάντων — Τα πλαίσια διεπαφής χρήστη και οι βρόχοι παιχνιδιών μπορούν να χρησιμοποιούν κορουτίνες για να εκφράσουν τη διαδοχική λογική που εκτείνεται σε πολλαπλά πλαίσια ή συμβάντα.
Παράλυση — Οι tokenizers και οι parserers μπορούν ναco_yieldtokens ή AST κόμβους καθώς επεξεργάζονται την είσοδο, διατηρώντας σιωπηρά την κατάσταση ανάλυσης.
Οι κορουτίνες και τα νήματα λύνουν διάφορα προβλήματα:
Χρησιμοποιήστε κορουτίνες όταν: χρειάζεστε συνεργασία πολλαπλών εργασιών, τεμπέλικη αξιολόγηση ή ασύγχρονες εισόδους/εξόδους χωρίς την επιβάρυνση των νημάτων του λειτουργικού συστήματος. Οι κορουτίνες δεν έχουν επιβάρυνση εναλλαγής περιβάλλοντος, δεν χρειάζονται πρωταρχικά στοιχεία συγχρονισμού και μπορούν να κλιμακωθούν σε εκατομμύρια ταυτόχρονες εργασίες.
Χρησιμοποιήστε νήματα όταν: χρειάζεστε πραγματικό παραλληλισμό μεταξύ των πυρήνων της CPU για εργασία συνδεδεμένη με CPU ή όταν χρειάζεστε προληπτικό προγραμματισμό (μια κορουτίνα που δεν αναστέλλεται ποτέ θα μπλοκάρει το νήμα της).
Βέλτιστη πρακτική: Χρησιμοποιήστε κορουτίνες για ταυτόχρονη σύνδεση I/O και νήματα για παραλληλισμό με δέσμευση CPU. Πολλοί διακομιστές υψηλής απόδοσης συνδυάζουν και τα δύο: μια ομάδα νημάτων εκτελεί κορουτίνες, όπου κάθε κορουτίνα χειρίζεται μία σύνδεση χρησιμοποιώντας τοco_awaitγια I/O.
Κρεμασμένες αναφορές: Οι παράμετροι κορουτίνας αντιγράφονται στο πλαίσιο, αλλά οι αναφορές σε προσωρινές μπορεί να κρεμούν. Να λαμβάνετε πάντα παραμέτρους ανά τιμή ή να διασφαλίζετε ότι το αντικείμενο αναφοράς ζει περισσότερο από την κορουτίνα.
Εκχώρηση σωρού: Κάθε κορουτίνα εκχωρεί ένα πλαίσιο στο σωρό. Για πολύ καυτές, βραχύβιες κορουτίνες, αυτό μπορεί να είναι ένα πρόβλημα απόδοσης (αν και οι μεταγλωττιστές μπορούν να βελτιστοποιήσουν μέσω του Heap Allocation eLision Optimization — HALO).
Δυσκολία εντοπισμού σφαλμάτων: Οι στοίβες κορουτίνας είναι πιο δύσκολο να επιθεωρηθούν σε προγράμματα εντοπισμού σφαλμάτων. Η κατάσταση αναστολής βρίσκεται στο σωρό, όχι στη στοίβα κλήσεων. Χρησιμοποιήστε καταγραφή και δομημένο ταυτόχρονο για να διατηρήσετε τη δυνατότητα εντοπισμού σφαλμάτων.
Χωρίς τυπικούς τύπους βιβλιοθήκης: Το C++20 παρέχει τα πρωτόγονα γλώσσας αλλά όχι τυπικό τύποGeneratorήTask. Πρέπει να γράψετε τη δική σας ή να χρησιμοποιήσετε μια βιβλιοθήκη. Το C++23 προσθέτει τοstd::generator.
- Οι κορουτίνες είναι λειτουργίες με δυνατότητα αναστολής — διακόπτουν παύση με το
co_yield/co_awaitκαι συνεχίζουν αργότερα - Το μοτίβο γεννήτριας παράγει τεμπέλικες ακολουθίες: κάθε
co_yieldαναστέλλει και αποδίδει μία τιμή - Το
co_awaitαναστέλλεται μέχρι να ολοκληρωθεί μια λειτουργία ασυγχρονισμού — οι τρεις μέθοδοι του αναμενόμενου ελέγχουν τη συμπεριφορά - Τα
promise_typeκαιcoroutine_handleαποτελούν το μηχάνημα που συνδέει την κορουτίνα με τον καλούντα - Οι κορουτίνες είναι συνεργάσιμες (χωρίς προκαταβολή) και ελαφριές (χωρίς νήμα λειτουργικού συστήματος) — ιδανικές για ταυτόχρονη σύνδεση I/O
- Το C++20 παρέχει μόνο primitives. χρησιμοποιήστε
std::generator(C++23) ή βιβλιοθήκες όπως το cppcoro για τον κώδικα παραγωγής
Κουίζ — Δοκιμάστε τις γνώσεις σας
(20 XP)1. Ποια λέξη-κλειδί κάνει μια συνάρτηση κορουτίνα στο C++20;
2. Ποιος είναι ο σκοπός του`promise_type::yield_value(T)`;
3. Πώς διαφέρουν οι κορουτίνες από τα νήματα;