انتقل إلى المحتوى

C++20 كوروتين

فهم coroutines كوظائف قابلة للتعليق. تعرف على co_yield وco_return وco_await ونمط المولد وحالات الاستخدام العملي مثل أجهزة الإدخال/الإخراج غير المتزامنة وأجهزة الحالة.

ما هي الكوروتينات؟

coroutine هي وظيفة يمكنها تعليق تنفيذها واستئنافها لاحقًا. على عكس الوظائف العادية التي تعمل من البداية إلى النهاية، يمكن للكوروتينات أن تتوقف مؤقتًا في منتصف التنفيذ، وتؤدي إلى نتائج متوسطة، وتستمر من حيث توقفت. يختلف هذا اختلافًا جوهريًا عن سلاسل العمليات: تكون coroutines تعاونية (تختار متى يتم تعليقها)، وخفيفة الوزن (لا حاجة إلى خيط نظام التشغيل)، وحتمية (لا توجد سباقات بيانات من الوصول المتزامن).

يقدم C++20 دعم coroutine على مستوى اللغة بثلاث كلمات رئيسية جديدة: co_yield (تعليق وإنتاج قيمة)، co_return (أكمل coroutine بقيمة نهائية)، و co_await (تعليق حتى تكتمل عملية المزامنة). أي وظيفة تحتوي على إحدى هذه الكلمات الأساسية تصبح تلقائيًا كوروتين.

نمط المولد

المولد عبارة عن كوروتين ينتج سلسلة من القيم بتكاسل. تقوم كل مكالمة إلى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;
}

آلات Coroutine: الوعد والتعامل

يحتوي كل كوروتين على مكونين رئيسيين:

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(). يعيش إطار coroutine في الكومة ويحتوي على كافة المتغيرات المحلية ونقطة التعليق.

التدفق هو: يقوم المتصل بإنشاء coroutine ->initial_suspend-> مكالمات المتصلresume()-> يعمل coroutine حتىco_yield/co_return/co_await-> يعود التحكم إلى المتصل -> تكرار.

co_await والكائنات المنتظرة

يقومco_awaitبتعليق coroutine حتى تكتمل العملية غير المتزامنة. يمكن استخدام أي نوع يقوم بتنفيذ واجهة 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;
}

حالات الاستخدام العملي

تتفوق Coroutines في العديد من سيناريوهات العالم الحقيقي:

المولدات/التسلسلات البطيئة — قم بإنتاج القيم حسب الطلب دون حساب التسلسل بأكمله مقدمًا. مثالية للتسلسلات اللانهائية، وتحليل الملفات، وخطوط أنابيب البيانات.

الإدخال/الإخراج غير المتزامن — يمكن لخوادم الشبكة واستعلامات قاعدة البيانات وعمليات الملفات إكمالco_awaitدون حظر سلاسل العمليات. توفر المكتبات مثلcppcoroوlibunifexوBoost.Asio بدايات الإدخال/الإخراج الجاهزة للكوروتين.

أجهزة الحالة — يمثل كلco_yieldأوco_awaitحالة انتقالية. تحافظ المتغيرات المحلية للكوروتين بشكل طبيعي على الحالة بين التحولات، مما يؤدي إلى التخلص من بنيات الحالة اليدوية.

البرمجة المبنية على الأحداث — يمكن لأطر عمل واجهة المستخدم وحلقات اللعبة استخدام coroutines للتعبير عن المنطق المتسلسل الذي يمتد إلى إطارات أو أحداث متعددة.

التحليل — يمكن لموزعي الرموز المميزة والموزعين رموزco_yieldأو عقد AST أثناء معالجة الإدخال، مع الحفاظ على حالة التحليل ضمنيًا.

أفضل ممارسة

Coroutines والخيوط تحل مشاكل مختلفة:

استخدم coroutines عندما: تحتاج إلى مهام متعددة تعاونية، أو تقييم بطيء، أو إدخال/إخراج غير متزامن دون تحميل سلاسل عمليات نظام التشغيل. لا تحتوي Coroutines على حمل إضافي لتبديل السياق، ولا حاجة إلى عناصر أولية للمزامنة، ويمكن أن تتسع لملايين المهام المتزامنة.

استخدم سلاسل العمليات عندما: تحتاج إلى توازٍ حقيقي عبر مراكز وحدة المعالجة المركزية للعمل المرتبط بوحدة المعالجة المركزية، أو عندما تحتاج إلى جدولة وقائية (سيؤدي الروتين الذي لا يتوقف أبدًا إلى حظر سلاسل العمليات الخاصة به).

أفضل الممارسات: استخدم coroutines للتزامن المرتبط بالإدخال/الإخراج والخيوط للتوازي المرتبط بوحدة المعالجة المركزية (CPU). تجمع العديد من الخوادم عالية الأداء بين الاثنين معًا: يقوم تجمع مؤشرات الترابط بتشغيل coroutines، حيث يتعامل كل coroutine مع اتصال واحد باستخدامco_awaitللإدخال/الإخراج.

خطأ شائع

المراجع المتدلية: يتم نسخ معلمات Coroutine في الإطار، لكن المراجع إلى المؤقتات يمكن أن تتدلى. قم دائمًا بأخذ المعلمات حسب القيمة أو تأكد من أن الكائن المشار إليه يتجاوز عمر الكوروتين.

تخصيص الكومة: يخصص كل كوروتين إطارًا على الكومة. بالنسبة للكوروتينات الساخنة جدًا وقصيرة العمر، يمكن أن يكون هذا مصدر قلق للأداء (على الرغم من أن المترجمين يمكنهم التحسين عبر Heap Allocation eLision Optimization - HALO).

صعوبة التصحيح : من الصعب فحص مكدسات Coroutine في مصححات الأخطاء. الحالة المعلقة موجودة في الكومة، وليس في مكدس الاستدعاءات. استخدم التسجيل والتزامن المنظم للحفاظ على إمكانية تصحيح الأخطاء.

لا توجد أنواع مكتبات قياسية: يوفر C++20 أساسيات اللغة ولكن لا يوجد نوعGeneratorأوTaskقياسي. يجب أن تكتب بنفسك أو تستخدم المكتبة. يضيف C++23std::generator.

النقاط الرئيسية
  • Coroutines هي وظائف قابلة للتعليق — فهي تتوقف مؤقتًا معco_yield/co_awaitوتستأنف لاحقًا
  • يُنتج نمط المولد تسلسلات كسولة: يعلق كلco_yieldقيمة واحدة ويقدمها
  • يتم تعليقco_awaitحتى تكتمل العملية غير المتزامنة - تتحكم الطرق الثلاثة المنتظرة في السلوك
  • يشكلpromise_typeوcoroutine_handleالآلية التي تربط الكوروتين بالمتصل به
  • تعد Coroutines تعاونية (بدون استباقية) وخفيفة الوزن (بدون مؤشر ترابط نظام التشغيل) — مثالية للتزامن المرتبط بالإدخال/الإخراج
  • يوفر C++20 العناصر الأولية فقط؛ استخدمstd::generator(C++23) أو مكتبات مثل cppcoro لرمز الإنتاج

اختبار — اختبر معرفتك

(20 نقطة خبرة)

1. ما هي الكلمة الأساسية التي تجعل الوظيفة coroutine في C++20؟

2. ما هو الغرض من`promise_type::yield_value(T)`؟

3. كيف تختلف coroutines عن المواضيع؟