رفتن به محتوا

کوروتین‌های ‎C++‎20

کوروتین‌ها را به‌عنوان توابع قابل تعلیق درک کنید. co_yield، co_return، co_await، الگوی مولد (Generator)، و موارد استفاده عملی مانند ورودی/خروجی ناهمگام و ماشین‌های حالت را بیاموزید.

کوروتین‌ها چیستند؟

یک کوروتین (Coroutine) تابعی است که می‌تواند اجرای خود را تعلیق کند و بعداً ازسرگیری شود. برخلاف توابع معمولی که از ابتدا تا انتها اجرا می‌شوند، کوروتین‌ها می‌توانند در میانه اجرا متوقف شوند، نتایج میانی ارائه دهند، و از همان‌جایی که متوقف شدند ادامه دهند. این اساساً با نخ‌ها (Threads) متفاوت است: کوروتین‌ها مشارکتی هستند (خودشان زمان تعلیق را انتخاب می‌کنند)، سبک هستند (نیاز به نخ سیستم‌عامل نیست)، و قطعی هستند (مسابقه داده از دسترسی همزمان وجود ندارد).

‎C++‎20 پشتیبانی از کوروتین را در سطح زبان با سه کلیدواژه جدید معرفی می‌کند: co_yield (تعلیق و تولید یک مقدار)، co_return (تکمیل کوروتین با یک مقدار نهایی)، و co_await (تعلیق تا تکمیل یک عملیات ناهمگام). هر تابعی که شامل یکی از این کلیدواژه‌ها باشد به‌صورت خودکار یک کوروتین است.

الگوی مولد (Generator)

یک مولد (Generator) کوروتینی است که یک دنباله از مقادیر را به‌صورت تنبل تولید می‌کند. هر فراخوانی 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() را فراهم می‌کند. فریم کوروتین روی هیپ (Heap) قرار دارد و شامل تمام متغیرهای محلی و نقطه تعلیق است.

جریان کار: فراخواننده کوروتین ایجاد می‌کند -> initial_suspend -> فراخواننده resume() را فراخوانی می‌کند -> کوروتین تا co_yield/co_return/co_await اجرا می‌شود -> کنترل به فراخواننده برمی‌گردد -> تکرار.

co_await و اشیای قابل انتظار (Awaitable)

co_await یک کوروتین را تا تکمیل یک عملیات ناهمگام تعلیق می‌کند. هر نوعی که رابط Awaitable را پیاده‌سازی کند می‌تواند با co_await استفاده شود. این پایه و اساس چارچوب‌های ورودی/خروجی ناهمگام است:

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

موارد استفاده عملی

کوروتین‌ها در چندین سناریوی دنیای واقعی برتری دارند:

مولدها / دنباله‌های تنبل — مقادیر را بر حسب تقاضا بدون محاسبه کل دنباله از قبل تولید می‌کنند. ایده‌آل برای دنباله‌های بی‌نهایت، تجزیه فایل و خطوط لوله داده.

ورودی/خروجی ناهمگام — سرورهای شبکه، پرس‌وجوهای پایگاه داده و عملیات فایل می‌توانند بدون مسدود کردن نخ‌ها، co_await تکمیل کنند. کتابخانه‌هایی مانند cppcoro، libunifex و Boost.Asio اولیه‌های ورودی/خروجی آماده کوروتین را فراهم می‌کنند.

ماشین‌های حالت — هر co_yield یا co_await نمایانگر یک انتقال حالت است. متغیرهای محلی کوروتین به‌طور طبیعی حالت را بین انتقال‌ها حفظ می‌کنند و ساختارهای حالت دستی را حذف می‌کنند.

برنامه‌نویسی رویداد‌محور — چارچوب‌های رابط کاربری و حلقه‌های بازی می‌توانند از کوروتین‌ها برای بیان منطق ترتیبی که چندین فریم یا رویداد را شامل می‌شود استفاده کنند.

تجزیه (Parsing) — توکنایزرها و تجزیه‌کننده‌ها می‌توانند توکن‌ها یا گره‌های AST را هنگام پردازش ورودی co_yield کنند و حالت تجزیه را به‌صورت ضمنی حفظ کنند.

بهترین شیوه

کوروتین‌ها و نخ‌ها مشکلات متفاوتی را حل می‌کنند:

از کوروتین‌ها استفاده کنید وقتی: به چندوظیفگی مشارکتی، ارزیابی تنبل، یا ورودی/خروجی ناهمگام بدون سربار نخ‌های سیستم‌عامل نیاز دارید. کوروتین‌ها سربار تعویض زمینه ندارند، نیاز به قفل‌های همگام‌سازی نیست، و می‌توانند تا میلیون‌ها وظیفه همزمان مقیاس‌پذیر شوند.

از نخ‌ها استفاده کنید وقتی: به موازی‌سازی واقعی در هسته‌های CPU برای کار وابسته به CPU نیاز دارید، یا وقتی نیاز به زمان‌بندی پیشگیرانه دارید (کوروتینی که هرگز تعلیق نمی‌کند نخ خود را مسدود خواهد کرد).

بهترین شیوه: از کوروتین‌ها برای همزمانی وابسته به ورودی/خروجی و از نخ‌ها برای موازی‌سازی وابسته به CPU استفاده کنید. بسیاری از سرورهای با عملکرد بالا هر دو را ترکیب می‌کنند: یک استخر نخ، کوروتین‌ها را اجرا می‌کند، جایی که هر کوروتین یک اتصال را با استفاده از co_await برای ورودی/خروجی مدیریت می‌کند.

دام

ارجاعات آویزان: پارامترهای کوروتین به فریم کپی می‌شوند، اما ارجاعات به اشیای موقتی می‌توانند آویزان شوند. همیشه پارامترها را به‌صورت مقدار بگیرید یا اطمینان حاصل کنید که شیء ارجاع‌شده بیشتر از کوروتین عمر می‌کند.

تخصیص هیپ: هر کوروتین یک فریم روی هیپ تخصیص می‌دهد. برای کوروتین‌های بسیار پرتکرار و کوتاه‌عمر، این می‌تواند نگرانی عملکردی باشد (اگرچه کامپایلرها می‌توانند از طریق HALO — بهینه‌سازی حذف تخصیص هیپ — بهینه‌سازی کنند).

دشواری اشکال‌زدایی: پشته‌های کوروتین در دیباگرها سخت‌تر بررسی می‌شوند. حالت تعلیق روی هیپ است، نه پشته فراخوانی. از ثبت‌وقایع (Logging) و همزمانی ساختاریافته برای حفظ قابلیت اشکال‌زدایی استفاده کنید.

نبود انواع کتابخانه استاندارد: ‎C++‎20 فقط اولیه‌های زبانی را فراهم می‌کند اما نوع استاندارد Generator یا Task ندارد. باید خودتان بنویسید یا از یک کتابخانه استفاده کنید. ‎C++‎23 std::generator را اضافه می‌کند.

نکات کلیدی
  • کوروتین‌ها توابع قابل تعلیق هستند — با co_yield/co_await متوقف می‌شوند و بعداً ازسرگیری می‌شوند
  • الگوی مولد (Generator) دنباله‌های تنبل تولید می‌کند: هر co_yield تعلیق می‌کند و یک مقدار تحویل می‌دهد
  • co_await تا تکمیل یک عملیات ناهمگام تعلیق می‌کند — سه متد شیء قابل انتظار (Awaitable) رفتار را کنترل می‌کنند
  • promise_type و coroutine_handle مکانیزمی را تشکیل می‌دهند که کوروتین را به فراخواننده‌اش متصل می‌کند
  • کوروتین‌ها مشارکتی (بدون پیشگیری) و سبک (بدون نخ سیستم‌عامل) هستند — ایده‌آل برای همزمانی وابسته به ورودی/خروجی
  • ‎C++‎20 فقط اولیه‌ها را فراهم می‌کند؛ از std::generator (‎C++‎23) یا کتابخانه‌هایی مانند cppcoro برای کد تولیدی استفاده کنید

آزمون — دانش خود را بسنجید

(20 امتیاز)

1. کدام کلیدواژه یک تابع را در ‎C++‎20 به کوروتین تبدیل می‌کند؟

2. هدف `promise_type::yield_value(T)` چیست؟

3. کوروتین‌ها چه تفاوتی با نخ‌ها دارند؟