C++20新特新——01协程的入门及一些语法知识

发布于:2025-05-07 ⋅ 阅读:(11) ⋅ 点赞:(0)

一、补充知识点:C++20引入的指定初始化器

        C++20 引入了 指定初始化器(Designated Initializers) 语法,允许在初始化聚合类型(如结构体或数组)时,通过成员名称显式指定初始化值。

语法要求

  • 仅适用于聚合类型

        指定初始化器只能用于初始化 聚合类型(没有用户自定义构造函数、私有/保护的非静态成员、基类或虚函数的结构体/类)。

  • 成员名称必须匹配

初始化器中使用的名称必须是聚合类型的成员名称。

  • 严格顺序要求

        C++ 不允许打乱成员的初始化顺序,必须按照成员在聚合类型中的声明顺序进行初始化。这与 C 语言不同(C 允许任意顺序)。

  • 可跳过成员

        可以跳过某些成员的初始化,未显式初始化的成员会被默认初始化(基本类型初始化为 0,类类型调用默认构造函数)

接下来给出一些用例,帮助我们更好的了解这个语法的使用:

struct Point {
    int x;
    int y;
    int z;
};

// 使用指定初始化器初始化
Point p1 = {.x = 10, .y = 20};       // z 被默认初始化为 0
Point p2 = {.x = 5, .y = 15, .z = 25};

// 错误:初始化顺序与声明顺序不一致
Point p_err = {.y = 20, .x = 10};  // 编译错误!

// 错误:使用了不存在的成员名称
Point p_err2 = {.a = 10};           // 编译错误!

// 错误:跳过 x 直接初始化 y 和 z(违反顺序规则)
Point p = {.y = 20, .z = 30}; // 编译失败!

二、补充知识点:C++11promise与future

问题:什么是std:promise这一类型呢?有什么作用?

        std::promise 是 C++11 引入的多线程编程工具,用于在线程间异步传递数据或异常。它通常与 std::future 配合使用,允许一个线程生成结果,另一个线程通过 future 获取该结果。

std::promise 的基本结构

template <typename T>
class promise;
  • T 是传递的数据类型(如 int, string, 或 void)。
  • 若 T 为 void,表示无数据传递,仅用于同步。

共享状态std::promise 和关联的 std::future 共享一个内部状态,用于存储值或异常。
单次写入:每个 promise 只能设置一次值或异常,重复设置会抛出 std::future_error。

std::promise<int> p;
std::future<int> f = p.get_future(); // 获取关联的 future

future 通过 get() 阻塞等待结果,或通过 wait() 仅等待。 

1. 设置值

std::promise<int> p;
p.set_value(42); // 存储值到共享状态

此时std:future可以从线程中获取到对应的值;

#include <iostream>
#include <thread>
#include <future>

void worker(std::promise<int> result_promise) {
    int result = 42 * 2;
    result_promise.set_value(result); // 传递结果
}

int main() {
    std::promise<int> p;
    std::future<int> f = p.get_future();

    std::thread t(worker, std::move(p)); // 转移所有权到线程

    std::cout << "Result: " << f.get() << std::endl; // 输出 84
    t.join();
    return 0;
}

2. 设置异常

try {
    // 可能抛出异常的代码
} catch (...) {
    p.set_exception(std::current_exception()); // 捕获并存储异常
}

需要注意的是设置异常的时候,此时std::promise<void>中的参数为void; 

void worker(std::promise<void> p) {
    try {
        throw std::runtime_error("Something went wrong!");
    } catch (...) {
        p.set_exception(std::current_exception()); // 传递异常
    }
}

int main() {
    std::promise<void> p;
    std::future<void> f = p.get_future();

    std::thread t(worker, std::move(p));

    try {
        f.get(); // 触发异常
    } catch (const std::exception& e) {
        std::cerr << "Exception: " << e.what() << std::endl;
    }
    t.join();
}

3.设置信号

当设置信号的时候,此时promise实例化的参数为void;

void 类型的特殊性:

  • std::promise<void> 的模板参数为 void,表示它不需要存储任何实际数据。
  • set_value() 不接收任何参数,仅用于标记关联的 std::future 的状态为“就绪”(ready)。

信号的本质:

  • 子线程通过 future.wait() 或 future.get() 感知到 future 状态变为“就绪”,从而解除阻塞。
  • 整个过程不涉及数据传递,只通过状态变化实现线程间同步。

例如下面这个例子:用于线程之间同步信息

std::promise<void> start_signal;
std::future<void> start_future = start_signal.get_future();

void worker() {
    start_future.wait(); // 等待主线程发送启动信号
    // 执行任务...
}

int main() {
    std::thread t(worker);
    std::this_thread::sleep_for(std::chrono::seconds(1));
    start_signal.set_value(); // 通知子线程启动
    t.join();
}

只有当主线程通过set_value,发送对应的值,此时子线程收到,然后开始执行任务,否则会一直阻塞! 

接下来我们这里再回顾一下C++11的std::async的作用和用法

三、补充知识点:C++11std::async与future

 std::async 是 C++11 引入的一个函数模板,用于简化异步任务的执行和管理。它允许开发者在后台启动一个函数(或可调用对象),并通过 std::future 对象获取异步操作的结果。

#include <future>

template <class Fn, class... Args>
std::future<typename std::result_of<Fn(Args...)>::type>
async(std::launch policy, Fn&& fn, Args&&... args);
  • 参数

    • policy: 启动策略(控制任务执行方式)。

    • fn: 要执行的函数或可调用对象。(可以是函数指针、仿函数、lambda表达式

    • args: 传递给 fn 的参数。

  • 返回值std::future,用于获取异步任务的结果。

关于policy启动策略,有以下几种参数可以供我们进行选择:

启动策略(std::launch

  1. std::launch::async
    立即在新线程中异步执行任务。若资源不足可能抛出 std::system_error

  2. std::launch::deferred
    延迟执行,直到调用 future::get() 或 future::wait() 时在调用线程中同步执行。

  3. 默认策略(不显式指定)
    允许实现选择 async 或 deferred,依赖编译器和系统资源。

下面我们给一个示例代码:

#include <iostream>
#include <future>
#include <chrono>

int compute(int a, int b) {
    std::this_thread::sleep_for(std::chrono::seconds(1)); // 模拟耗时操作
    return a + b;
}

int main() {
    // 异步执行 compute(2, 3),策略为 std::launch::async
    std::future<int> fut = std::async(std::launch::async, compute, 2, 3);
    std::cout << "主线程继续执行...\n";
    int result = fut.get(); // 阻塞直到获取结果
    std::cout << "结果: " << result << std::endl; // 输出 5
    return 0;
}

上面的代码中:

std::future<int> fut = std::async(std::launch::async, compute, 2, 3);

调用上面这行代码,此时会异步执行上面的任务;

但是如果想要获取对应的结果:

 int result = fut.get(); // 阻塞直到获取结果

此时会获取对应的结果; 

问题:std::async与C++线程库里的thread有什么区别?

这两个虽然都是异步线程使用,但是底层机制和使用场景不一样;(deepseek)

1. 线程管理方式

std::thread

  • 直接创建操作系统线程,每次调用 std::thread 都会生成一个独立的新线程。

  • 手动管理:必须显式调用 join() 或 detach()(主线程要对子线程进行等待),否则程序会因未定义行为终止。

  • 无返回值:无法直接获取线程函数的返回值,需通过共享变量或回调传递结果。

std::async

  • 基于任务抽象:通过 std::async 提交的任务可能复用线程池(由实现决定),不一定每次创建新线程。

  • 自动管理:返回的 std::future 对象在析构时会隐式等待任务完成(若未显式 get() 或 wait(),可能导致隐式阻塞)。

  • 支持返回值:通过 std::future<T> 直接获取异步操作的结果;

这里最主要的是async可以获取到其他线程的结果!

2. 启动策略

std::thread

  • 强制异步:线程始终立即启动,与主线程并行执行。

std::async

  • 灵活策略:通过 std::launch 参数控制启动方式:

    • std::launch::async:强制异步启动(类似 std::thread)。

    • std::launch::deferred:延迟执行,直到调用 future.get() 或 future.wait() 时同步运行

    • 默认策略是 std::launch::async | std::launch::deferred,由实现决定。

3. 异常处理

std::thread

  • 线程内部异常不会自动传递到主线程,若未捕获会导致程序终止。

  • 需在子线程中手动捕获异常并通过共享变量传递错误信息。

std::async

  • 异常会自动传播到主线程:调用 future.get() 时,若子任务抛出异常,会在主线程重新抛出。

  • 示例:

auto future = std::async(std::launch::async, []{
    throw std::runtime_error("子线程异常");
});
try {
    future.get(); // 在主线程捕获异常
} catch (const std::exception& e) {
    std::cout << "捕获异常: " << e.what() << std::endl;
}

四、协程的引入

问题:什么是协程?

协程实际上就是一段可以支持挂起(suspend)和恢复的(resume)的程序,一般而言,也就是支持挂起和恢复的函数!

接下来我们看一个关于普通函数的例子:

void Fun() {
  std::cout << 1 << std::endl;
  std::cout << 2 << std::endl;
  std::cout << 3 << std::endl;
  std::cout << 4 << std::endl;
}

此时,在运行这个函数中我们知道是不能暂停的!

这个暂停当然不包括打断点然后运行,是指在正常的操作中,程序是运行是不会间断的;

问题:那么协程是如何实现函数能够暂停和恢复的?

下面给出一个协程的伪代码:

Result Coroutine() {
  std::cout << 1 << std::endl;
  co_await std::suspend_always{};
  std::cout << 2 << std::endl;
  std::cout << 3 << std::endl;
  co_await std::suspend_always{};
  std::cout << 4 << std::endl;
};

这里我们认识几个主要的名词:

  • Result: 这里表示的是协程的返回值(结构是是一个类包含一个内部类,下面会讲解);
  • co_wait: 这是一个关键词,需要记住的是co_wait + 等待体(即后面接的是等待体);

问题:co_wait的作用是什么?

这里的核心作用是:挂起当前协程,等待异步操作完成,并在合适的时机恢复协程的后续执行。

其中,具体的实现是通过等待体来实现的!

接下来,我们不着急看协程的等待题,因为还有几个概念需要理解一下;

1. 协程的状态

协程被称为轻量化的线程,因此对于进程来说,其有运行态,阻塞态等,因此协程肯定也有自己的状态;

这里我们举一个例子来类比协程:整个音频文件协程进行对比

音频

协程

音频文件

协程体

音频播放

协程执行

播放暂停

协程挂起

播放恢复

协程恢复

播放一场

协程异常

播放完成

协程返回

        与进程一样,当协程挂起,然后后面我们对其进行恢复的时候,那么此时当前的信息就一定要保存下来,也就是协程对应的上下文信息(例如代码执行的位置,这样恢复的时候才可以继续执行后续的代码);

        因此,当我们执行对应的协程代码的时候,此时协程进行挂起,哪么C++的协程就会立马调用operator new来开辟一块空间保存这些信息这块空间或者空间中保存的对象又被称为协程的状态!

        协程的状态不仅会保存挂起点的位置(也就是挂起点),还会保存协程体对应的参数,例如下面的示例代码:

Result Coroutine(int start_value) {
  std::cout << start_value << std::endl;
  co_await std::suspend_always{};
  std::cout << start_value + 1 << std::endl;
};

也就是说,上面的形参start_value也会被存储到协程的状态当中!

因此,这里我们总结一下,协程的状态主要包含下面两种信息

  • 挂起点的位置;
  • 协程体对应的形参;

2. 协程的挂起

在C++中,我们采用co_wait + 等待体来管理协程的挂起操作;

而等待体的实现需要三个函数,这里我们分别介绍下这三个函数:

await_ready
bool await_ready();

上面这个函数比较简单,我们不需要向其中传递相关的参数; 

  • await_ready 返回 bool 类型,如果返回 true,则表示已经就绪,无需挂起;
  • 否则表示需要挂起。

在标准库当中,提供了两个简单的等待体,就是根据上面这个函数来实现的:

struct suspend_never {
    constexpr bool await_ready() const noexcept {
        return true;  // 返回 true,总是不挂起
    }
    ...
};

struct suspend_always {
    constexpr bool await_ready() const noexcept {
        return false; // 返回 false,总是挂起
    }

    ...
};
  • struct suspend_always 表示总是挂起;
  • struct suspend_never 表示总是不挂起;

问题:这里为什么用constexptr来修饰函数?这个关键词起什么作用?

        constexptr这个关键字用来声明编译时的常量或者编译时的表达式,从而将计算从运行时转换到编译时;

        而上面的例子中,此时constexprt修饰的是整个函数!(而不是具体的返回值)

        加上这个关键词后的作用是:表示该函数可以在编译期间进行求值。如果函数满足 constexpr 的条件(如无动态内存分配、无未定义行为等),编译器会在编译时直接计算其返回值,并将函数调用替换为常量值

        例如上面的suspend_never这个结构体内,此时该函数被修饰后,则在编译期间该函数的返回值恒为true,此时编译器内部就不会对其进行判断(是否需要挂起?),而是直接在编译期间将代码判定为恒不挂起!从而实现优化的效果!

问题:这里noexcept的作用是什么?

        这个关键字用于声明该函数不会抛异常,所以在函数的编译期间,编译器可以异常的处理机制(如栈展开代码),从而提高性能

await_suspend

        只有当await_ready返回值为false,此时说明协程已经被挂起,所以此时该函数会被调用!(如果await_ready返回true,此时不会调用该函数)

??? await_suspend(std::coroutine_handle<> coroutine_handle);

由于该函数的返回值存在多种情况,这里我们用??? 表示返回值;

其中,参数coroutine_handle表示协程的句柄,用来接下来恢复协程;

coroutine_handle.resume();

接下来我们分析其几种返回值的情况:

  • 返回 void 类型或者返回 true,表示当前协程挂起之后将执行权还给当初调用或者恢复当前协程的函数;(也就是说,此时如果主函数创建协程,那么对应的执行权会归还给主函数;)
  • 返回 false,则恢复执行当前协程。注意此时不同于 await_ready 返回 true 的情形,此时协程已经挂起,await_suspend 返回 false 相当于挂起又立即恢复;
  • 返回其他协程的 coroutine_handle 对象,这时候返回的 coroutine_handle 对应的协程被恢复执行;
  • 抛出异常,此时当前协程恢复执行,并在当前协程当中抛出异常;
await_resume

在协程体恢复之后,此时会调用await_resume;

??? await_resume();

且await_resume()的返回值也是不确定的,并且返回值会作为整个等待体的返回值;

3. 协程的等待体

接下来我们看下面一个非常简单的等待体:

struct Awaiter {
  int value;

  bool await_ready() {
    // 协程挂起
    return false;
  }

  void await_suspend(std::coroutine_handle<> coroutine_handle) {
    // 切换线程
    std::async([=](){
      using namespace std::chrono_literals;
      // sleep 1s
      std::this_thread::sleep_for(1s); 
      // 恢复协程
      coroutine_handle.resume();
    });
  }

  int await_resume() {
    // value 将作为 co_await 表达式的值
    return value;
  }
};
Result Coroutine() {
  std::cout << 1 << std::endl;
  std::cout << co_await Awaiter{.value = 1000} << std::endl;
  std::cout << 2 << std::endl; // 1 秒之后再执行
};

其中,这个等待体中的 await_ready()和 await_resume这两个函数在这里的作用都比较容易理解,这里再重点讲一下await_suspend这个函数的作用:

  • 首先传入参数:std::coroutine_handle<> coroutine_handle,其是协程的句柄,这里是为了接下来可以将协程进行恢复或者摧毁;
  • 通过async创建一个异步线程,新的异步线程休眠了1s,然后恢复协程;
  • using namespace std::chrono_literals;这里是为了简写1s的写法;

        整体相当于调用到co_wait的时候,协程被挂起,执行await_suspend()里面的任务,然后协程被恢复,且await_resume的返回值会作为整个函数体的返回值进行返回;

4. 协程的返回值对象的构建

        一个函数是不是协程,这里是根据其返回值是否满足协程的规则来判断的,如果满足,那么此时这个函数就会被编译成协程!

template <class _Ret, class = void>
struct _Coroutine_traits {};

template <class _Ret>
struct _Coroutine_traits<_Ret, void_t<typename _Ret::promise_type>> {
    using promise_type = typename _Ret::promise_type;
};

template <class _Ret, class...>
struct coroutine_traits : _Coroutine_traits<_Ret> {};

        满足协程的规则:也就是说协程的返回值能够实例化上面的_Coroutine_traits模板!则函数就是协程!

        而上面的规则看起来很复杂,但是实际上协程的返回值需要满足下面几点即可:

  • 必须包含一个嵌套类型 promise_type
  • promise_type 必须提供 get_return_object() 方法,用于生成协程的返回值对象;
  • promise_type 既可以是直接定义在 _Ret 当中的类型,也可以通过 using 指向已经存在的其他外部类型;

返回值Result 对象的创建是由 promise_type 负责的,我们需要定义一个 get_return_object 函数来处理对 Result 对象的创建:

因此,此时协程的返回值大致就如下结构所示:

struct Result {
  struct promise_type {

    Result get_return_object() {
      // 创建 Result 对象
      return {};
    }

    ...
  };
};

        通过get_return_type获取到协程的返回值对象;

        跟一般的函数不一样,一般的函数的返回值是返回之前被创建的!

        而协程的返回值是协程的状态被创建的时候就立马创建出来的!也就是说,协程的状态被创建出来之后,会立即构造 promise_type 对象,进而调用 get_return_object 来创建返回值对象。

5. 协程体的返回值     

接下来我们讲一下协程体的返回值,其主要是通过Result内的promise_type定义一个return_value()函数来决定!

        对于返回一个值的情况,需要在 promise_type 当中定义一个函数:

??? return_value();

协程体返回整数 

例如协程体返回整数:

struct Result {
  struct promise_type {

    void return_value(int value) {
      ...
    }

    ...

  };
};

此时需要注意的是,这里的返回值类型为void,但是真正的返回值是存储在了形参value当中

Result Coroutine() {
  ...
  co_return 1000;
};

此时,我们通过在协程体内通过co_return返回对应的整数值;

例如上面,这时1000 会作为参数传入,即 return_value 函数的参数 value 的值为 1000。 

这个value可以存到 promise_type 对象当中,外部的调用者可以获取到。

协程体返回void

struct Result {
  struct promise_type {
    
    void return_void() {
      ...
    }

    ...
  };
};

这里对应的是return_void()没有参数这种情况;

Result Coroutine() {
  ...
  co_return;
};

此时协程体内部可以通过co_return来退出协程体; 

协程体抛出异常

        协程体除了正常返回以外,也可以抛出异常。异常实际上也是一种结果的类型,因此处理方式也与返回结果相似。我们只需要在 promise_type 当中定义一个函数,在异常抛出时这个函数就会被调用到:

struct Result {
  struct promise_type {
    
    void unhandled_exception() {
      exception_ = std::current_exception(); // 获取当前异常
    }

    ...
  };
};

promise_type中除了 get_return_object()和return_void两个成员函数,还要求有:initial_suspend和final_suspend这两个函数来管理协程启动和结束时的挂起行为;

initial_suspend

作用:是用来控制协程的初始挂起行为,在协程的函数体执行之前,决定协程是否立刻挂起!

  • 影响协程的启动方式

    • 返回 std::suspend_always:协程创建后立即挂起,需手动调用 coruntine.resume() 启动。

    • 返回 std::suspend_never:协程创建后立即开始执行,无需手动恢复。

struct Generator {
    struct promise_type {
        // 协程创建后立即挂起,等待手动恢复
        std::suspend_always initial_suspend() { return {}; }
        // ... 其他成员
    };
};

        当 initial_suspend() 返回 std::suspend_always 时,协程会在开始执行协程体内的任何代码之前立即挂起,即使尚未遇到 co_await。此时协程的初始状态是挂起的,必须通过手动调用 coroutine_handle::resume() 才能恢复执行。 

final_suspend

作用:是用来控制协程的结束时的挂起行为,在协程的函数体执行之后,决定协程是否立刻挂起!

  • 影响协程的销毁方式

    • 返回 std::suspend_always:协程结束后保持挂起,需手动调用 coruntine.destory()销毁协程帧。

    • 返回 std::suspend_never:协程自动销毁,但需确保不再访问协程句柄

struct Generator {
    struct promise_type {
        // 协程结束后保持挂起,允许异步获取结果
        std::suspend_always final_suspend() noexcept { return {}; }
        // ... 其他成员
    };
};

这里的return {},实际上就是通过{}构造一个临时对象,实际上与下面两种调用方法效果一样;

{}这里是C++11的统一初始化;

return std::suspend_always{};
return suspend_always();


网站公告

今日签到

点亮在社区的每一天
去签到