1+1=10

扬长避短 vs 取长补短

C++协程小记

接前面Python 协程小记JavaScript异步函数,回头在看看C++中的协程。

C++2020引入协程,C++2023引入std::generator,通过例子,学习一下。

C++ 生成器

C++2023 引入std::generator,所以支持下面这种写法(尽管截至目前只有GCC libstdc++ 支持。MSVC在experimental中):

#include <generator>
#include <iostream>

std::generator<int> my_generator() {
    co_yield 1;
    co_yield 2;
    co_yield 3;
}

int main() {
    for (int value : my_generator()) {
        std::cout << value << std::endl;
    }
    return 0;
}
// 1
// 2
// 3

和同时期的其他语言比,C++中的generator晚了好多年。

其他语言

  • python 支持生成器(python 2.2,2001年):
def my_generator():
    yield 1
    yield 2
    yield 3

for value in my_generator():
    print(value)
  • C# 支持生成器(C#2.0,2005):
using System;

public static class Program
{
    public static System.Collections.Generic.IEnumerable<int> Generate()
    {
        yield return 1;
        yield return 2;
        yield return 3;
    }

    public static void Main()
    {
        foreach (var value in Generate())
        {
            Console.WriteLine(value);
        }
    }
}

using System;
using System.Collections;

public class MyGenerator // : IEnumerable
{
    public IEnumerator GetEnumerator()
    {
        yield return 1;
        yield return 2;
        yield return 3;
    }
}

class Program
{
    static void Main(string[] args)
    {
        foreach (var value in new MyGenerator())
        {
            Console.WriteLine(value);
        }
    }
}
  • javascript支持生成器(es2015):
function* myGenerator() {
    yield 1;
    yield 2;
    yield 3;
}

for (const value of myGenerator()) {
    console.log(value);
}

协程

std::generator 是 C++2023才引入的。截至目前(2024年3月),主流编译器也只支持C++2020的大部分特性。

借用一张图

image-20240306205438890

A调用B,但是B又恢复A的执行。就像一个多次进入和退出的函数,和普通函数(function)、子程序(subroutine)相比,还是挺神奇的。

协程函数?

一个函数只要包含下面任何关键字,就是协程

  • co_await:暂停协程执行,直到等待的操作(另一个协程或异步操作)完成。
  • co_yield:暂停协程执行并向调用方返回一个值(协程恢复时从该位置继续执行),用于按顺序生成一系列值。
  • co_return:从协程返回值并标志其完成(提前退出)。

这几个关键字看起来挺简单的,co_yieldco_awaitco_return主要配合完成交出控制权,但是协程函数的返回值(比如前面的std::generator)有特定的要求,显得很复杂。协程暂停后,需要靠它来恢复协程。

还是先看例子

生成器例子

在C++2020中,提供了coroutine的基础设施,但没有具体的协程类。

要实现上面类似生成器的功能,需要自行定义一个MyGenerator类,而后将其作为协程函数的返回值才能用:

#include <coroutine>
#include <iostream>

template<typename T>
struct MyGenerator {
    struct promise_type {
        T current_value;

        MyGenerator get_return_object() {
            return MyGenerator{ 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 = value;
            return {};
        }

        void return_void() {}

        void unhandled_exception() {
            std::terminate();
        }
    };

    std::coroutine_handle<promise_type> coroutine;

    explicit MyGenerator(std::coroutine_handle<promise_type> coro) : coroutine(coro) {}

    ~MyGenerator() {
        if (coroutine) {
            coroutine.destroy();
        }
    }

    T current_value() const {
        return coroutine.promise().current_value;
    }

    struct iterator {
        MyGenerator& gen;

        bool operator!=(const iterator&) const {
            return gen.coroutine && !gen.coroutine.done();
        }

        const T& operator*() const {
            return gen.current_value();
        }

        void operator++() {
            if (gen.coroutine) {
                gen.coroutine.resume();
            }
        }
    };

    iterator begin() {
        if (coroutine) {
            coroutine.resume();
        }
        return { *this };
    }

    iterator end() {
        return { *this };
    }
};

MyGenerator<int> my_generator() {
    co_yield 1;
    co_yield 2;
    co_yield 3;
}

int main() {
    for (int value : my_generator()) {
        std::cout << value << std::endl;
    }

    return 0;
}

C++2020的这个东西,就不像是给普通用户用的。需要先封装成库才行。上面为了用range for,定义了迭代器。

即使去掉range for支持,代码还是挺乱:

#include <coroutine>
#include <iostream>

template<typename T>
struct MyGenerator {
    struct promise_type {
        T current_value;

        MyGenerator get_return_object() {
            return MyGenerator{ 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 = value;
            return {};
        }

        void return_void() {}

        void unhandled_exception() {
            std::terminate();
        }
    };

    std::coroutine_handle<promise_type> coroutine;

    explicit MyGenerator(std::coroutine_handle<promise_type> coro) : coroutine(coro) {}

    ~MyGenerator() {
        if (coroutine) {
            coroutine.destroy();
        }
    }

    bool move_next() {
        if (!coroutine.done()) {
            coroutine.resume();
            return !coroutine.done(); // Check if the coroutine is done after resuming
        }
        else {
            return false;
        }
    }

    T current_value() const {
        return coroutine.promise().current_value;
    }
};

MyGenerator<int> my_generator() {
    co_yield 1;
    co_yield 2;
    co_yield 3;
}

int main() {
    MyGenerator<int> gen = my_generator();
    while (gen.move_next())
    {
        std::cout << gen.current_value() << std::endl;
    }

    return 0;
}

涉及到的类有点多,先放个图:

coroutine-cpp20

拆开看看?promise_type

C++协程中的promise概念 和 std::promise 以及 std::future 完全不是一个东西。

要定义协程函数,先需要定义一个结构体作为协程函数的返回值类型,该结构体需要有一个promise_type结构体成员

#include <iostream>
#include <coroutine>

struct MyGenerator {
    struct promise_type {
        std::suspend_never initial_suspend() { std::cout << "initial_suspend()\n"; return {}; }
        std::suspend_always final_suspend() noexcept { std::cout << "final_suspend()\n"; return {}; }

        std::suspend_never yield_value(int value) {std::cout << "yield_value() "<<value<<"\n"; return {}; }
        void return_void() { std::cout << "return_void()\n"; }

        MyGenerator get_return_object() {std::cout << "get_return_object()\n"; return {}; }

        void unhandled_exception() {std::terminate(); }
    };
};

MyGenerator simpleCoroutine() {
    co_yield 1;
    co_yield 2;
    co_yield 3;
}

int main() {
    std::cout << "before coro" << std::endl;
    auto coro = simpleCoroutine();
    std::cout << "after coro" << std::endl;

    return 0;
}

// before coro
// get_return_object()
// initial_suspend()
// yield_value() 1
// yield_value() 2
// yield_value() 3
// return_void()
// final_suspend()
// after coro

promise_type` 结构体定义了协程的 Promise 接口,它包含了协程执行的状态和用于处理异步等待的函数。Promise 接口有以下成员函数:

  • get_return_object(): 返回用于保存协程状态的对象。
  • initial_suspend(): 返回协程开始时 暂停状态的 awaitable 对象。用于决定协程开始执行时,是否立即暂停。
  • final_suspend(): 返回协程结束时 暂停状态的 awaitable 对象。用于决定协程结束执行时,是否立即暂停。
  • yield_value(): 接受co_yield值并返回中途交出控制权时暂停状态的 awaitable 对象。
  • return_void(): 描述协程返回void时的行为(没有co_return 或者co_return;)。协程执行完毕,且没有中途交出控制权的情况下,被调用。
  • return_value():描述协程返回非void类型时的行为(使用语句 co_return v;)。协程执行完毕,且没有中途交出控制权的情况下,被调用。
  • unhandled_exception(): 处理未处理的异常。协程内发生异常且没有被catch时,函数被调用。

上面例子中,刻意将initial_suspend()yield_value()返回值都设置为std::suspend_never,使得协程初始化和遇到co_yield时都不暂停,直接执行到底。不然在这个例子中,一旦暂停后,没有办法恢复它。final_suspend的返回值std::suspend_always还是其他都可以,取觉于要不要这时候使用协程资源。

协程函数调用promise的成员,如果协程函数展开,可以看作下面这样的伪代码:

{
    promise-type promise promise-constructor-arguments ;
    try {
        co_await promise.initial_suspend() ;
        function-body
    } catch ( ... ) {
        if (!initial-await-resume-called)
            throw ;
        promise.unhandled_exception() ;
    }
final-suspend :
    co_await promise.final_suspend() ;
}
  • co_await :最基础
  • co_yield v; :可以想象成 co_await promise.yield_value(v);
  • co_return;co_return v; :分别对应 promise.return_void()promise.return_value(v)。但是它们都不返回Awaitable,也就是不能用暂停协程。

拆开看看?std::coroutine_handle

promise_type 是对内的,对外,需要std::coroutine_handle。这个结构体还挺复杂,一个promise的特化,一个void特化。

template <class = void>
struct coroutine_handle;

template <>
struct coroutine_handle<void> {
    //...
    bool done() const {
        return __builtin_coro_done(_Ptr);
    }

    void operator()() const {
        __builtin_coro_resume(_Ptr);
    }

    void resume() const {
        __builtin_coro_resume(_Ptr);
    }

    void destroy() const {
        __builtin_coro_destroy(_Ptr);
    }

private:
    void* _Ptr = nullptr;
};

template <class _Promise>
struct coroutine_handle {
    static coroutine_handle from_promise(_Promise& _Prom) {
        const auto _Prom_ptr  = const_cast<void*>(static_cast<const volatile void*>(_STD addressof(_Prom)));
        const auto _Frame_ptr = __builtin_coro_promise(_Prom_ptr, 0, true);
        coroutine_handle _Result;
        _Result._Ptr = _Frame_ptr;
        return _Result;
    }
    constexpr operator coroutine_handle<>() const {
        return coroutine_handle<>::from_address(_Ptr);
    }

   //....

   _Promise& promise() const {
        return *reinterpret_cast<_Promise*>(__builtin_coro_promise(_Ptr, 0, false));
    }

private:
    void* _Ptr = nullptr;
}

协程暂停之后,需要调用coroutine_handleresume()才能恢复。

这个coroutine_handle是在promise_type中的get_return_object()中创建的:

        MyGenerator get_return_object() {
            return MyGenerator{ std::coroutine_handle<promise_type>::from_promise(*this) };
        }
  • 协程函数被调用时,通过get_return_object()获取 MyGenerator 类型的对象,并返回该 MyGenerator 对象。
  • MyGenerator构造时会创建coroutine_handle对象,并作为结构体成员存储。
  • coroutine_handle的析构函数并不会释放资源,必须调用destroy()

注意模板的使用,先声明了模板默认参数类型void,后创建其特化版本:

#include <iostream>

template <class = void>
struct S;

template <>
struct S<void>
{
    int v = 1;
};

template<class T>
struct S
{
    T v = 2;

    operator S<>() const {
        return S<>{.v = static_int<int>(v) };
    }
};

int main() {
    S a0;
    S<double> a1;
    S a2 = a1;
    std::cout << a0.v << " " << a1.v << " " << a2.v << std::endl; // 1 2 2
    return 0;
}

拆开看看? Awaitable

标准库中的suspend_neversuspend_always

struct suspend_never {
    constexpr bool await_ready() const {
        return true;
    }

    constexpr void await_suspend(coroutine_handle<>) const {}
    constexpr void await_resume() const {}
};

struct suspend_always {
    constexpr bool await_ready() const {
        return false;
    }

    constexpr void await_suspend(coroutine_handle<>) const {}
    constexpr void await_resume() const {}
};

它们实现了特定的 awaitable 接口:

  • await_ready(): 返回一个布尔值,指示等待是否立即完成。如果等待立即完成,返回 true;否则返回 false
  • await_suspend(): 描述协程在等待时的暂停状态。返回一个 std::coroutine_handle 或类似对象,指示协程的挂起点。这个函数通常会保存等待时的状态,以便在等待完成后恢复协程的执行。
  • await_resume(): 返回等待完成后的结果。当等待完成时,此函数用于获取最终结果。

用自定义类取代std::suspend_always

#include <coroutine>
#include <iostream>

struct MyAwaitable
{
    bool await_ready() const noexcept {
        return false;
    }

    void await_suspend(std::coroutine_handle<>) const noexcept { std::cout << "await_suspend()\n"; }
    void await_resume() const noexcept { std::cout << "await_resume()\n"; }
};

template<typename T>
struct MyGenerator {
    struct promise_type {
        T current_value;

        MyGenerator get_return_object() {
            return MyGenerator{ std::coroutine_handle<promise_type>::from_promise(*this) };
        }

        MyAwaitable initial_suspend() { return {}; }
        MyAwaitable final_suspend() noexcept { return {}; }

        MyAwaitable yield_value(T value) {
            current_value = value;
            return {};
        }

        void return_void() {}

        void unhandled_exception() {
            std::terminate();
        }
    };

    std::coroutine_handle<promise_type> coroutine;

    explicit MyGenerator(std::coroutine_handle<promise_type> coro) : coroutine(coro) {}

    ~MyGenerator() {
        if (coroutine) {
            coroutine.destroy();
        }
    }

    bool move_next() {
        if (!coroutine.done()) {
            coroutine.resume();
            return !coroutine.done(); // Check if the coroutine is done after resuming
        }
        else {
            return false;
        }
    }

    T current_value() const {
        return coroutine.promise().current_value;
    }
};

MyGenerator<int> my_generator() {
    co_yield 1;
    co_yield 2;
    co_yield 3;
    co_return;
}

int main() {
    MyGenerator<int> gen = my_generator();
    while (gen.move_next())
    {
        std::cout << gen.current_value() << std::endl;
    }

    return 0;
}

结果:

await_suspend()
await_resume()
await_suspend()
1
await_resume()
await_suspend()
2
await_resume()
await_suspend()
3
await_resume()
await_suspend()

在这个例子中,由于 final_suspend()返回值不是std::suspend_never,所以final_suspend后我们还可以访问协程信息。如下:

int main() {
    MyGenerator<int> gen = my_generator();
    while (gen.move_next())
    {
        std::cout << gen.current_value() << std::endl;
    }
    std::cout << gen.coroutine.done() << std::endl;

    return 0;
}

如果 返回值是std::suspend_never,协程到时会被自动销毁,再尝试方位协程信息会崩溃。

另外,Awaitable用在co_await后面,所以前面的例子,甚至都可以这么写:

MyGenerator<int> my_generator() {
    co_await MyAwaitable();
    co_await MyAwaitable();
    co_await MyAwaitable();
    co_await MyAwaitable();
    co_await MyAwaitable();
    co_await MyAwaitable();

    co_yield 1;
    co_yield 2;
    co_yield 3;
    co_return;
}

编译环境

VS2019 与 C++20

尽管 VS2019 支持 C++20,使用VS2019的IDE的话,需要手动选择C++语言标准为 /std:c++20/std:c++latest。另外,确保构建是x64而不是x86或win32。不然都会有类似下面的错误

namespace std contains no member suspend_always

在VS2019下,generator在experimental中:

#include <experimental/generator>
#include <iostream>

std::experimental::generator<int> my_generator() {
    co_yield 1;
    co_yield 2;
    co_yield 3;
}

int main() {
    for (int value : my_generator()) {
        std::cout << value << std::endl;
    }
    return 0;
}

GCC 协程选项

g++ -std=c++20 -fcoroutines my_coro.cpp
g++ -std=c++20 my_coro.cpp

其中,在GCC10中,需要通过-fcoroutines 用于启用协程支持 (experimental)。GCC11起,不再需要这个选项。

参考

  • https://en.cppreference.com/w/cpp/links
  • https://en.cppreference.com/w/cpp/compiler_support/20
  • https://gcc.gnu.org/projects/cxx-status.html
  • https://en.cppreference.com/w/cpp/language/coroutines
  • https://en.cppreference.com/w/cpp/coroutine
  • https://www.scs.stanford.edu/~dm/blog/c++-coroutines.html#compiling-code-using-coroutines
  • https://lewissbaker.github.io/2017/09/25/coroutine-theory
  • https://itnext.io/c-20-coroutines-complete-guide-7c3fc08db89d
  • https://www.scs.stanford.edu/~dm/blog/c++-coroutines.pdf

Comments