C++ 协程(Coroutines)基础:`co_await`, `co_yield`, `co_return` (C++20)

好的,下面开始我们的C++协程讲座!

各位观众老爷,今天我们来聊聊C++20引入的协程,这玩意儿听起来高大上,实际上也没那么玄乎。咱们要搞清楚co_await, co_yield, 和 co_return这三个核心关键字,它们就像协程的发动机,控制着协程的暂停、恢复和结束。

协程是啥?跟线程有啥区别?

想象一下,你是一个厨师,同时要烤面包、煮咖啡、煎鸡蛋。如果你是单线程模式,你就得按顺序来,烤完面包才能煮咖啡,煮完咖啡才能煎鸡蛋。这效率多低啊!

但如果你会协程,你就可以先开始烤面包,然后发现要等面包发酵,就暂停一下,去煮咖啡,咖啡煮好后,发现鸡蛋还没到时间,又暂停一下,回去烤面包。这样,你就可以在多个任务之间来回切换,充分利用时间。

简单来说,协程是一种用户态的线程,它允许你在函数执行过程中暂停执行,并稍后从暂停的地方恢复执行。关键是,协程的切换是由程序员控制的,而不是像线程那样由操作系统调度。

特性 线程 协程
调度者 操作系统 程序员/协程库
上下文切换 需要操作系统内核介入,开销大 用户态切换,开销小
并发性 真正的并行,需要多核CPU支持 伪并行,单线程内实现并发
适用场景 CPU密集型任务,需要真正并行处理 IO密集型任务,高并发,避免线程切换开销
内存占用 每个线程需要独立的栈空间,占用较多内存 多个协程可以共享一个线程的栈空间,占用较少内存

co_await:暂停一下,等等我!

co_await是协程中最常用的关键字,它的作用是暂停当前协程的执行,等待一个awaitable对象完成。这个awaitable对象通常代表一个异步操作,比如网络请求、文件读取等等。

#include <iostream>
#include <future>
#include <coroutine>

// 一个简单的 awaitable 对象
struct MyAwaitable {
  int value;

  bool await_ready() const {
    // 如果已经准备好,就返回 true,否则返回 false
    return false; // 总是暂停,模拟异步操作
  }

  void await_suspend(std::coroutine_handle<> h) {
    // 在这里进行异步操作,并在操作完成后恢复协程
    std::cout << "暂停中..." << std::endl;
    std::thread([h, this]() {
      std::this_thread::sleep_for(std::chrono::seconds(1)); // 模拟耗时操作
      value = 42; // 设置结果
      h.resume(); // 恢复协程
    }).detach();
  }

  int await_resume() {
    // 返回异步操作的结果
    std::cout << "恢复执行,结果是: " << value << std::endl;
    return value;
  }
};

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() {}
  };
};

// 一个简单的协程
Task MyCoroutine() {
  std::cout << "协程开始执行" << std::endl;
  MyAwaitable awaitable{0};
  int result = co_await awaitable; // 暂停协程,等待 awaitable 对象完成
  std::cout << "协程继续执行,result = " << result << std::endl;
  co_return;
}

int main() {
  MyCoroutine();
  std::this_thread::sleep_for(std::chrono::seconds(2)); // 确保异步操作完成
  return 0;
}

这个例子中,MyAwaitable是一个简单的awaitable对象,它模拟了一个异步操作。await_ready()总是返回false,表示操作没有准备好,需要暂停协程。await_suspend()函数负责启动异步操作,并在操作完成后调用h.resume()恢复协程。await_resume()函数返回异步操作的结果。

MyCoroutine()是一个协程,它使用co_await暂停执行,等待MyAwaitable对象完成。当MyAwaitable对象完成时,协程会从暂停的地方恢复执行,并继续执行后面的代码。

co_yield:给外面扔个东西!

co_yield用于生成一个序列的值,类似于Python中的yield。它允许你创建一个生成器,每次调用生成器都会返回序列中的下一个值。

#include <iostream>
#include <coroutine>

// 一个简单的生成器
template <typename T>
struct Generator {
  struct promise_type {
    T value_;
    std::exception_ptr exception_;

    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 {}; }
    void unhandled_exception() { exception_ = std::current_exception(); }
    std::suspend_always yield_value(T value) {
      value_ = value;
      return {};
    }
    void return_void() {}
  };

  using handle_type = std::coroutine_handle<promise_type>;

  Generator(handle_type h) : handle_(h) {}
  ~Generator() { if (handle_) handle_.destroy(); }

  Generator(const Generator&) = delete;
  Generator& operator=(const Generator&) = delete;

  bool next() {
    handle_.resume();
    return !handle_.done();
  }

  T value() { return handle_.promise().value_; }

private:
  handle_type handle_;
};

// 一个简单的生成器协程
Generator<int> MyGenerator(int start, int end) {
  for (int i = start; i <= end; ++i) {
    co_yield i; // 产生一个值
  }
}

int main() {
  auto generator = MyGenerator(1, 5);
  while (generator.next()) {
    std::cout << "生成的值是: " << generator.value() << std::endl;
  }
  return 0;
}

这个例子中,MyGenerator()是一个生成器协程,它使用co_yield产生一个整数序列。每次调用generator.next()都会恢复协程的执行,直到遇到co_yield语句,此时协程会暂停执行,并将i的值返回。当循环结束后,协程会自动结束。

co_return:我溜了!

co_return用于从协程中返回值或者结束协程的执行。它的作用类似于普通函数的return语句,但是它不会立即结束协程的执行,而是会先执行一些清理工作,比如销毁局部变量等等。

#include <iostream>
#include <coroutine>

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() {}
    };
};

// 一个简单的协程
Task MyCoroutine() {
  std::cout << "协程开始执行" << std::endl;
  co_return; // 结束协程
  std::cout << "这行代码不会被执行" << std::endl;
}

int main() {
  MyCoroutine();
  return 0;
}

这个例子中,MyCoroutine()是一个简单的协程,它使用co_return结束协程的执行。co_return后面的代码不会被执行。

协程的框架:Promise!

要理解协程的运作方式,Promise 是个绕不开的概念。每个协程都和一个 Promise 对象关联,这个 Promise 对象负责管理协程的状态、返回值、异常等等。

Promise 是一个模板类,你需要提供一个 promise_type 结构体,这个结构体定义了协程的行为。promise_type 结构体必须包含以下成员函数:

  • get_return_object():返回协程的返回值。
  • initial_suspend():决定协程是否在开始时暂停执行。
  • final_suspend():决定协程在结束时是否暂停执行。
  • return_void()return_value(T value):设置协程的返回值。
  • unhandled_exception():处理协程中未捕获的异常。
  • yield_value(T value) (如果使用 co_yield):产生一个值。

让我们看一个更完整的例子,结合了 co_await 和 Promise:

#include <iostream>
#include <future>
#include <coroutine>

struct MyAwaitable {
    int value;
    std::promise<int> promise;

    bool await_ready() const { return false; }
    void await_suspend(std::coroutine_handle<> h) {
        std::thread([h, this]() {
            std::this_thread::sleep_for(std::chrono::seconds(1));
            promise.set_value(42); // 设置结果
            value = 42;
            h.resume(); // 恢复协程
        }).detach();
    }
    int await_resume() {
        return promise.get_future().get();
    }
};

struct Task {
    struct promise_type {
        int result;
        std::exception_ptr exception;
        std::coroutine_handle<promise_type> coroutine_handle;

        Task get_return_object() {
            return Task{std::coroutine_handle<promise_type>::from_promise(*this)};
        }
        std::suspend_never initial_suspend() { return {}; } // 立即执行
        std::suspend_always final_suspend() noexcept { return {}; } // 暂停,直到销毁

        void return_value(int value) {
            result = value;
        }
        void unhandled_exception() {
            exception = std::current_exception();
        }
    };

    std::coroutine_handle<promise_type> handle;
};

Task MyCoroutine() {
    std::cout << "协程开始" << std::endl;
    MyAwaitable awaitable{0};
    int result = co_await awaitable;
    std::cout << "协程继续,result = " << result << std::endl;
    co_return result * 2;
}

int main() {
    Task task = MyCoroutine();
    std::this_thread::sleep_for(std::chrono::seconds(2));
    //访问结果
    std::cout << "最终结果" << task.handle.promise().result << std::endl;
    return 0;
}

在这个例子中,Task 结构体包含了 promise_type,它负责管理协程的返回值。MyCoroutine() 协程使用 co_await 等待 MyAwaitable 完成,并将结果乘以 2 后通过 co_return 返回。

协程的优势:

  • 更高的并发性: 协程可以在单线程内实现并发,避免了线程切换的开销。
  • 更低的内存占用: 多个协程可以共享一个线程的栈空间,减少了内存占用。
  • 更简洁的代码: 协程可以简化异步编程的代码,使代码更易于理解和维护。

协程的缺点:

  • 调试困难: 协程的执行流程比较复杂,调试起来比较困难。
  • 学习曲线陡峭: 协程的概念比较抽象,需要一定的学习成本。
  • 不适合CPU密集型任务: 协程是伪并行,本质上还是单线程,不适合CPU密集型任务。

使用场景:

  • 网络编程: 协程可以用于编写高并发的网络服务器。
  • GUI编程: 协程可以用于处理GUI事件,避免阻塞UI线程。
  • 游戏开发: 协程可以用于实现游戏逻辑,例如AI、动画等等。

总结:

co_awaitco_yieldco_return 是 C++ 协程的三个核心关键字,它们分别用于暂停协程、生成值和结束协程。通过合理地使用这三个关键字,你可以编写出高效、简洁的异步代码。

协程是一个强大的工具,但是它也需要一定的学习成本。希望通过今天的讲解,你能对 C++ 协程有一个初步的了解,并在实际项目中尝试使用它。

记住,协程不是万能的,它有自己的适用场景和局限性。在选择使用协程之前,一定要仔细评估你的需求,并权衡利弊。

最后,祝大家编程愉快!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注