C++20新特性_协程(Coroutines)

文章目录

  • [第一章 C++20核心语法特性](#第一章 C++20核心语法特性)
    • [1.3 协程 (Coroutines)](#1.3 协程 (Coroutines))
      • [1.3.1 协程实现原理](#1.3.1 协程实现原理)
      • [1.3.2 协程举例](#1.3.2 协程举例)
      • [1.3.3 关键字解析](#1.3.3 关键字解析)
      • [1.3.4 线程和协程的使用场景](#1.3.4 线程和协程的使用场景)
      • [1.3.5 协程总结](#1.3.5 协程总结)

本文记录C++20新特性之协程(Coroutines)。

第一章 C++20核心语法特性

1.3 协程 (Coroutines)

为了提供异步代码的高效性,C++20引入了协程,它为异步编程提供了一种全新的范式。在C++20之前,处理异步操作(如网络IO),通常需要注册回调函数来处理数据。但是回调函数再次调用回调函数,甚至多次调用回调函数,就出现了"回调地狱",逻辑被分在了不同的回调函数中,导致逻辑分散。下面是一个 回调函数的例子:

cpp 复制代码
    class Reader
    {
    public:
        void read(const std::function<void(int)> & callback)
        {
            std::thread t([callback]() {
                this_thread::sleep_for(std::chrono::seconds(1)); // 模拟耗时操作

                // 接收到33 数据 
                int value = 33;

                callback(value); // 调用回调函数
            });

            t.join();
		}
    };

    // 回调函数
    void printValue(int value)
    {
		cout << "Received value: " << value << endl;
    }

    void test()
    {
        Reader reader;
        reader.read(printValue);
		// Received value: 33
    }

但是,如果回调函数层次增加,回调函数又调用了其他函数,这就是"回调地狱"下面代码中printValue又调用了其他函数,用来处理这个value值。

cpp 复制代码
    class Reader
    {
    public:
        void read(const std::function<void(int)> & callback)
        {
            std::thread t([callback]() {
                this_thread::sleep_for(std::chrono::seconds(1)); // 模拟耗时操作

                // 接收到33 数据 
                int value = 33;
                callback(value); // 调用回调函数
            });
            t.join();
		}
    };

    void handleValue(int value)
    {
		cout << "Handled value: " << value << endl;
    }

    // 回调函数
    void printValue(int value)
    {
		handleValue(value);
		//cout << "Received value: " << value << endl;
    }

    void test()
    {
        Reader reader;
        reader.read(printValue);
		// Received value: 33
    }

这个处理逻辑被到多个函数中实现,数据流难以追踪,异常处理比较困难。C++20中引入了协程,旨在结合"同步代码的可读性"与"异步代码的高效性"。 它允许函数在执行过程中挂起 (Suspend),并在稍后恢复 (Resume),而无需销毁栈帧或阻塞线程。

1.3.1 协程实现原理

协程就是一个不依赖于任何线程栈的函数,协程切换时,"协程帧"(局部变量,参数等)保存在堆上,任何线程都可以直接访问。

当在函数中调用 co_wait,co_yield或co_return时,这个函数就是一个协程。

C++协程的三个概念

1 Promise对象(promise_type) : 这是协程的控制中心,规定了协程启动时做什么、结束时做什么、遇到异常如何处理、以及如何处理返回值。

2 Awaitable (可等待对象):这是 co_await 后面跟着的对象。它决定了协程是否真的要挂起,以及挂起后该做什么(例如注册一个回调,等 I/O 完成后恢复协程)。这个可等待对象必须实现 await_ready,await_suspend,await_resume三个接口。

3 Coroutine Handle (std::coroutine_handle):这是一个轻量级指针,用来控制协程的恢复和销毁。

1.3.2 协程举例

下面实现一个协程,并分析程序的调用过程。

cpp 复制代码
    // 实现一个 生成器,
    struct Generator
    {
        // 声明 promise_type,这是编译器查找的约定名称
        struct promise_type;
		using handle_type = std::coroutine_handle<promise_type>; 
        // 用于在协程外部控制协程的执行和生命周期

        handle_type h;// 协程句柄

		// 构造函数与析构函数
		Generator(handle_type h_) 
            : h(h_) 
        {
        
        }
        ~Generator() // RAII 销毁协程 
        {
            if (h)
            {
				h.destroy();
            }
        }

        // 只能移动,不能拷贝
		Generator(const Generator&) = delete;
		Generator& operator=(const Generator&) = delete;
        Generator(Generator && obj) noexcept
            : h(obj.h)
        {
            obj.h = nullptr;
		}

        // 恢复 协程执行一次
        bool next()
        {
			h.resume();
			return !h.done();
        }

		// 获取当前值
        int getValue()
        {
			return h.promise().current_value;
        }

		// 2 定义 promise_type
        struct promise_type
        {
            int current_value;

            // 携程创建时 返回对象
            Generator get_return_object()
            {
				return Generator{ handle_type::from_promise(*this) };
            }

            // 协程启动时的行为:suspend_always 表示启动后立即挂起,等待手动调用 next
            std::suspend_always initial_suspend() { return {}; }

            // 协程结束时的行为
            std::suspend_always final_suspend() noexcept { return {}; }

            // 处理 co_yield 表达式
            std::suspend_always yield_value(int value) {
                current_value = value;
                return {}; // 挂起协程,将控制权交回调用者
            }

            // 处理 co_return
            void return_void() {}

            // 处理异常
            void unhandled_exception() {
                std::terminate();
            }
        };
    };

    // 创建协程 
    Generator sequence(int start, int step)
    {
        int i = start;
        while (true)
        {
            // 挂起执行,并返回 i。下次 resume 时从这里继续。
            co_yield i;
			i += step;

            if (i > 20)
            {
				co_return; // 结束协程
            }
        }
    }

    void test()
    {
        // 创建协程,此时处于挂起状态
        auto gen = sequence(0, 5);

        std::cout << "Start generator" << std::endl;

        // 手动驱动
        while (gen.next())
        {
			std::cout << "Generated value: " << gen.getValue() << std::endl;
        }

		std::cout << "Generator finished." << std::endl;

        /*
            Start generator
            Generated value: 0
            Generated value: 5
            Generated value: 10
            Generated value: 15
            Generated value: 20
            Generator finished.
        
        */
    }

分析上面代码的执行流程:

1 调用 Generator gen = sequence(0, 5); 时,编译器发现sequence内部有 co_yield,所以将sequence 函数看作一个协程。

2 创建协程状态:编译器在堆上分配一个 "协程帧",并将,promise_type(协程的控制器,用于定义协程的行为 )局部变量i, step等保存在协程帧中。

3 创建返回对象:调用get_return_object() 并返回一个Generator对象,这个对象中持有这个协程帧的句柄。

4 协程开始执行。当协程开始执行时,遇到第一件事就是挂起,因为std::suspend_always ,所以协程在执行 while之前,先挂起。

5 返回gen : sequence 函数调用返回,gen变量现在持有一个已经创建但处于初始挂起状态的协程。

此时, 一个协程初始化完毕。然后,当执行到 while (gen.next()) 时,在next()内部调用了resume(),唤醒协程,协程开始执行。

第一次执行到while ()循环中,

i = 0, 执行到 co_yield i后,会调用 yield_value(0) 函数,并将该函数中的current_value 设置为0,因为yield_value 返回 std::suspend_always,协程再次被挂起。

控制权再次交给main线程,在主线程继续手动驱动 gen.next(),检查done(),因为协程没有结束,只是挂起,因此返回false,继续执行gen.next()后,返回true. 主线程继续执行,进入while循环体中,current_value为0,打印Generated value = 0.

第二次,继续执行gen.next(),再次唤醒协程,这个过程一直重复,直到i=20后,执行i += step变为25后,执行 co_return,协程开始结束。结束时,先调用 h.promise. return_void(),执行 final_suspend(),协程再次挂起,但这次的协程是完成状态。

所以,当再次调用next()时,协程已经完整了,next()返回false,跳出while(),结束。

总结执行过程:while (gen.next()) 循环就像一个手动泵。每一次 next() 调用就是"泵"一下,驱动协程执行一小段(从一个挂起点到下一个挂起点),并让它产生一个值。当协程通过 co_return 正常结束后,next() 会返回 false,循环自然停止。

1.3.3 关键字解析

co_await : 尝试挂起当前协程;

co_yield : 挂起协程并向调用者传出一个值。

co_return :结束协程执行,返回一个最终值。

1.3.4 线程和协程的使用场景

**使用线程:**当需要进行大规模的、可并行的科学计算、数据处理、图像渲染等 CPU 密集型任务时,通过多线程利用多核 CPU 的并行处理能力。

**使用协程:**当需要处理大量的网络请求、文件读写、数据库访问等 I/O 密集型任务时。使用协程,一个线程就可以管理成千上万个并发连接,因为在等待 I/O 完成时,线程不会被阻塞,而是可以切换去执行另一个准备就绪的协程。

1.3.5 协程总结

**协程优势:**协程类似函数,但是没有栈,状态保存在堆中,内存开销极小,通常只有几十字节的堆内存,上下文切换比操作系统的线程快得多(纳秒级)。

相关推荐
Mr_WangAndy1 小时前
C++20新特性_Lambda 改进
c++20·c++20新特性·c++40周年·lambda表达式改进
Mr_WangAndy10 小时前
C++17 新特性_第二章 C++17 语言特性_std::any和string_view
c++·string_view·c++40周年·c++17新特性·c++新特性any
Mr_WangAndy13 小时前
C++17 新特性_第一章 C++17 语言特性___has_include,u8字符字面量
c++·c++40周年·c++17新特性·__has_include·u8字面量
Mr_WangAndy21 小时前
C++17 新特性_第二章 C++17标准库特性_std::invoke和std::invoke_result_t
c++·invoke·c++40周年·c++17新特性·invoke_result
Mr_WangAndy1 天前
C++14 新特性_第二章 C++14 标准库特性_std::exchange,std::quoted
c++·exchange·c++40周年·quoted·c++14新特性
Mr_WangAndy1 天前
C++17 新特性_第一章 C++17 语言特性_if constexpr,类模板参数推导 (CTAD)
c++·c++40周年·if constexpr·类模板参数推导 ctad·c++17新特性
止观止2 天前
告别“祖传C++”:开启你的现代C++之旅
c++·c++11·c++20·编程思想·现代c++
Mr_WangAndy2 天前
C++14新特性_第一章C++语言特性_Lambda初始化捕获,decltype(auto)
c++·c++40周年·lambda初始化捕获·decltype auto
Chrikk2 天前
【下篇】C++20 约束、NCCL 通信与并发模型
c++·c++20·c++40周年