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

C++20 कोरूटीन

कोरूटीन को रोके जा सकने वाले (suspendable) फ़ंक्शन के रूप में समझें। co_yield, co_return, co_await, जनरेटर पैटर्न और async I/O तथा स्टेट मशीन जैसे व्यावहारिक उपयोग सीखें।

कोरूटीन क्या हैं?

कोरूटीन (coroutine) एक ऐसा फ़ंक्शन है जो अपने निष्पादन को रोक (suspend) सकता है और बाद में फिर से शुरू (resume) किया जा सकता है। सामान्य फ़ंक्शन शुरू से अंत तक चलते हैं, जबकि कोरूटीन बीच में रुक सकते हैं, मध्यवर्ती नतीजे दे सकते हैं और जहाँ छोड़ा था वहीं से आगे बढ़ सकते हैं। यह थ्रेड से बुनियादी रूप से अलग है: कोरूटीन सहकारी (cooperative) होते हैं (वे ख़ुद चुनते हैं कि कब रुकना है), हल्के होते हैं (OS थ्रेड की ज़रूरत नहीं) और नियतात्मक (deterministic) होते हैं (समवर्ती एक्सेस से डेटा रेस नहीं होती)।

C++20 तीन नए कीवर्ड के साथ भाषा स्तर पर कोरूटीन सपोर्ट लाता है: co_yield (रुकना और एक वैल्यू देना), co_return (अंतिम वैल्यू के साथ कोरूटीन पूरा करना), और co_await (किसी async ऑपरेशन के पूरा होने तक रुकना)। इनमें से कोई भी कीवर्ड रखने वाला कोई भी फ़ंक्शन अपने-आप कोरूटीन बन जाता है।

जनरेटर पैटर्न

जनरेटर एक ऐसा कोरूटीन है जो मानों का अनुक्रम लेज़ी तरीक़े से बनाता है। co_yield की हर कॉल कोरूटीन को रोकती है और कॉलर तक एक वैल्यू पहुँचाती है। यह रहा एक पूरा, कंपाइल हो सकने वाला जनरेटर इम्प्लीमेंटेशन:

generator.cpp
#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;
}

कोरूटीन की मशीनरी: 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 ऑब्जेक्ट

co_await किसी कोरूटीन को तब तक रोकता है जब तक कोई असिंक्रोनस ऑपरेशन पूरा न हो जाए। जो भी टाइप Awaitable इंटरफ़ेस लागू करता है, उसे co_await के साथ इस्तेमाल किया जा सकता है। यही async I/O फ़्रेमवर्क की नींव है:

co_await_example.cpp
#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;
}

व्यावहारिक उपयोग

कोरूटीन कई वास्तविक परिदृश्यों में बेहतरीन साबित होते हैं:

जनरेटर / लेज़ी अनुक्रम — पूरा अनुक्रम पहले से गणना किए बिना ज़रूरत पड़ने पर मान बनाएँ। अनंत अनुक्रम, फ़ाइल पार्सिंग और डेटा पाइपलाइन के लिए आदर्श।

Async I/O — नेटवर्क सर्वर, डेटाबेस क्वेरी और फ़ाइल ऑपरेशन थ्रेड को ब्लॉक किए बिना पूरा होने पर co_await कर सकते हैं। cppcoro, libunifex और Boost.Asio जैसी लाइब्रेरी कोरूटीन-तैयार I/O प्रिमिटिव देती हैं।

स्टेट मशीन — हर co_yield या co_await एक स्टेट ट्रांज़िशन दर्शाता है। कोरूटीन के लोकल वेरिएबल ट्रांज़िशन के बीच स्टेट को स्वाभाविक रूप से सँजोकर रखते हैं, जिससे मैन्युअल स्टेट स्ट्रक्ट की ज़रूरत नहीं रहती।

इवेंट-ड्रिवन प्रोग्रामिंग — UI फ़्रेमवर्क और गेम लूप कोरूटीन का उपयोग करके ऐसा क्रमिक लॉजिक व्यक्त कर सकते हैं जो कई फ़्रेम या इवेंट तक फैला हो।

पार्सिंग — टोकनाइज़र और पार्सर इनपुट प्रोसेस करते समय टोकन या AST नोड co_yield कर सकते हैं और पार्सिंग स्टेट को अप्रत्यक्ष रूप से बनाए रखते हैं।

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

कोरूटीन और थ्रेड अलग-अलग समस्याएँ हल करते हैं:

कोरूटीन तब इस्तेमाल करें जब: आपको OS थ्रेड के ओवरहेड के बिना सहकारी मल्टीटास्किंग, लेज़ी इवैल्यूएशन या async I/O चाहिए। कोरूटीन में कॉन्टेक्स्ट-स्विच का ओवरहेड नहीं होता, किसी सिंक्रोनाइज़ेशन प्रिमिटिव की ज़रूरत नहीं पड़ती, और वे लाखों समवर्ती टास्क तक स्केल कर सकते हैं।

थ्रेड तब इस्तेमाल करें जब: आपको CPU-बाउंड काम के लिए CPU कोर में वास्तविक पैरेलिज़्म चाहिए, या जब आपको प्रीएम्प्टिव शेड्यूलिंग चाहिए (जो कोरूटीन कभी नहीं रुकता, वह अपने थ्रेड को ब्लॉक कर देगा)।

बेस्ट प्रैक्टिस: I/O-बाउंड कॉन्करेंसी के लिए कोरूटीन और CPU-बाउंड पैरेलिज़्म के लिए थ्रेड का उपयोग करें। कई हाई-परफ़ॉर्मेंस सर्वर दोनों को मिलाते हैं: एक थ्रेड पूल कोरूटीन चलाता है, जहाँ हर कोरूटीन I/O के लिए co_await का उपयोग करके एक कनेक्शन संभालता है।

सामान्य गलती

डैंगलिंग रेफ़रेंस: कोरूटीन के पैरामीटर फ़्रेम में कॉपी हो जाते हैं, लेकिन अस्थायी ऑब्जेक्ट के रेफ़रेंस डैंगल कर सकते हैं। पैरामीटर हमेशा वैल्यू से लें, या सुनिश्चित करें कि रेफ़र किया गया ऑब्जेक्ट कोरूटीन से ज़्यादा समय तक जीवित रहे।

हीप आवंटन: हर कोरूटीन हीप पर एक फ़्रेम आवंटित करता है। बहुत हॉट, कम समय तक जीने वाले कोरूटीन के लिए यह परफ़ॉर्मेंस की चिंता बन सकता है (हालाँकि कंपाइलर Heap Allocation eLision Optimization — HALO — से इसे ऑप्टिमाइज़ कर सकते हैं)।

डीबग करने में कठिनाई: डीबगर में कोरूटीन स्टैक को जाँचना ज़्यादा कठिन होता है। रुकी हुई अवस्था हीप पर होती है, कॉल स्टैक पर नहीं। डीबग करने की सुविधा बनाए रखने के लिए लॉगिंग और स्ट्रक्चर्ड कॉन्करेंसी का उपयोग करें।

कोई स्टैंडर्ड लाइब्रेरी टाइप नहीं: C++20 भाषा के प्रिमिटिव देता है, लेकिन कोई स्टैंडर्ड Generator या Task टाइप नहीं। आपको ख़ुद लिखना होगा या किसी लाइब्रेरी का उपयोग करना होगा। C++23 std::generator जोड़ता है।

मुख्य बातें
  • कोरूटीन रोके जा सकने वाले फ़ंक्शन हैं — वे co_yield/co_await से रुकते हैं और बाद में फिर शुरू होते हैं
  • जनरेटर पैटर्न लेज़ी अनुक्रम बनाता है: हर co_yield रुकता है और एक वैल्यू देता है
  • co_await तब तक रुकता है जब तक कोई async ऑपरेशन पूरा न हो — awaitable के तीन मेथड व्यवहार को नियंत्रित करते हैं
  • promise_type और coroutine_handle वह मशीनरी बनाते हैं जो कोरूटीन को उसके कॉलर से जोड़ती है
  • कोरूटीन सहकारी (कोई प्रीएम्प्शन नहीं) और हल्के (कोई OS थ्रेड नहीं) हैं — I/O-बाउंड कॉन्करेंसी के लिए आदर्श
  • C++20 केवल प्रिमिटिव देता है; प्रोडक्शन कोड के लिए std::generator (C++23) या cppcoro जैसी लाइब्रेरी का उपयोग करें

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

(20 XP)

1. C++20 में कौन-सा कीवर्ड किसी फ़ंक्शन को कोरूटीन बनाता है?

2. `promise_type::yield_value(T)` का उद्देश्य क्या है?

3. कोरूटीन थ्रेड से किस तरह अलग हैं?