(一)基本概念
协程(coroutine)是一种特殊的函数,它可以被暂停(suspend)、恢复执行(resume),并且一个协程可以被多次调用。c 中的协程属于stackless协程,即协程被suspend时不需要堆栈。c 20开始引入协程,围绕协程实现的相应组件较多,如co_wait、co_return、co_yield,promise,handle等组件,灵活性高,组件之间的关系也略复杂,这使得c 协程学习起来有一定难度。
协程与传统函数不同,普通函数是线程相关的,函数的状态跟线程紧密关联;而协程是线程无关的,它的状态与任何线程都没有关系。普通函数调用时,线程的栈上会记录函数的状态(参数、局部变量等),通过移动栈顶指针来完成;而协程的状态是保存在堆内存上的。当协程执行时,它跟普通函数一样依赖线程栈,但一旦暂停,其状态会独立保存在堆中,调用它的线程可以继续做其他事情,下次恢复执行时,协程可以由上次执行的线程执行,也可以由另外一个完全不同的线程执行。
(二)特点
- 非阻塞:协程可以在执行过程中暂停,允许其他协程运行,从而实现非阻塞的异步编程。
- 轻量级:协程的创建和切换开销较小,适合高并发场景。与传统的多线程相比,协程的创建和切换不需要操作系统的调度,开销远小于线程,并且可以在单个线程中实现高并发,避免了线程上下文切换的开销。
- 可读性高:使用协程可以使异步代码更易于理解和维护,避免了回调地狱(callback hell)。协程允许开发者以同步的编码风格编写异步代码,提高了代码的可读性和可维护性。
(三)应用场景
- 异步编程:c 20协程在异步编程中的应用非常广泛,它使得编写异步代码变得更加直观和简洁。可以使用
co_await
来等待异步操作的完成,而不需要使用回调函数或者promise/future模式。例如:
#include
#include
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 asynchronous_code() {
// 启动一个异步操作
// 这里简单模拟,实际中可能是一个耗时的异步函数
co_await std::suspend_always{
};
// 在异步操作完成之后,接着运行下面的代码
std::cout << "asynchronous operation completed." << std::endl;
}
int main() {
auto task = asynchronous_code();
// 这里需要手动处理协程的恢复等操作,实际中可能会有更完善的调度机制
return 0;
}
- 生成器:c 20协程也可以用来创建生成器,这些生成器可以在每次请求时生成新的值。可以创建一个在请求新值时才计算它们的无限序列。例如:
#include
#include
// 定义生成器类型
template
struct generator {
struct promise_type {
t current_value;
generator get_return_object() {
return {
}; }
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() {
}
};
bool move_next() {
// 恢复协程执行
handle.resume();
return !handle.done();
}
t current_value() {
return handle.promise().current_value;
}
std::coroutine_handle handle;
};
// 生成整数序列的生成器协程
generator integers(int start = 0) {
int i = start;
while (true) {
co_yield i ;
}
}
int main() {
auto gen = integers();
for (int i = 0; i < 5; i) {
if (gen.move_next()) {
std::cout << gen.current_value() << std::endl;
}
}
return 0;
}
- 并发与并行编程:c 20协程能很好地处理并发和并行编程。通过协程,可以在不阻塞线程的情况下等待操作完成,这在处理i/o操作或者网络请求时尤其有用。例如,在处理多个文件下载任务时:
#include
#include
#include
#include
// 模拟异步下载文件的函数
std::future download_file(const std::string& url) {
return std::async([url]() {
// 模拟下载耗时
std::this_thread::sleep_for(std::chrono::seconds(2));
std::cout << "downloaded: " << url << std::endl;
});
}
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 download_files(const std::vector& urls) {
std::vector> tasks;
for (const auto& url : urls) {
tasks.push_back(download_file(url));
}
for (auto& task : tasks) {
co_await std::suspend_always{
};
task.wait();
}
}
int main() {
std::vector urls = {
"url1", "url2", "url3"};
auto task = download_files(urls);
return 0;
}
二、c 协程精通知识
(一)高级特性
- 协程的状态机实现:当一个函数被声明为协程时,编译器会自动将其转换为一个状态机。状态机负责保存协程的执行状态,并在协程挂起和恢复时进行状态切换。状态机通常包含协程的局部变量、挂起点以及
promise_type
对象等信息。例如:
#include
#include
struct returnobject {
struct promise_type {
returnobject get_return_object() {
return {
}; }
std::suspend_never initial_suspend() {
return {
}; }
std::suspend_never final_suspend() noexcept {
return {
}; }
void unhandled_exception() {
}
void return_void() {
}
};
};
returnobject simple_coroutine() {
std::cout << "coroutine started" << std::endl;
co_await std::suspend_always{
};
std::cout << "coroutine resumed" << std::endl;
}
int main() {
auto coro = simple_coroutine();
// 这里需要手动处理协程的恢复等操作,实际中可能会有更完善的调度机制
return 0;
}
在这个例子中,simple_coroutine
函数被编译器转换为状态机,当遇到co_await std::suspend_always{}
时,协程挂起,保存当前状态,等待后续恢复执行。
2. 自定义promise对象和awaitable对象:
– promise对象:promise_type
是一个用户自定义的类型,用于控制协程的行为。每个协程都需要定义一个promise_type
,它负责创建协程的初始状态、在协程挂起时保存状态、在协程恢复时恢复状态、处理协程的返回值或异常以及控制协程的生命周期。例如:
#include
#include
struct mytask {
struct promise_type {
mytask get_return_object() {
return {
}; }
std::suspend_never initial_suspend() {
return {
}; }
std::suspend_never final_suspend() noexcept {
return {
}; }
void unhandled_exception() {
std::terminate(); }
void return_void() {
}
};
};
mytask my_coroutine() {
std::cout << "my coroutine started" << std::endl;
co_await std::suspend_always{
};
std::cout << "my coroutine resumed" << std::endl;
}
int main() {
auto task = my_coroutine();
// 这里需要手动处理协程的恢复等操作,实际中可能会有更完善的调度机制
return 0;
}
- **awaitable对象**:`awaitable`对象用于表示一个可以挂起的异步操作。当协程遇到`co_await`表达式时,它会检查`awaitable`对象是否已经完成。如果未完成,协程将挂起,直到`awaitable`对象完成。`awaitable`对象必须提供`await_ready()`、`await_suspend()`和`await_resume()`等成员函数。例如:
#include
#include
#include
struct awaitablefuture {
std::future future;
bool await_ready() const {
return future.wait_for(std::chrono::seconds(0)) == std::future_status::ready; }
void await_suspend(std::coroutine_handle<> handle) {
std::thread([this, handle]() mutable {
future.wait();
handle.resume();
}).detach();
}
int await_resume() {
return future.get(); }
};
std::future fetchdataasync() {
return std::async([]() {
std::this_thread::sleep_for(std::chrono::seconds(2));
return 42;
});
}
int asyncfetchdata() {
awaitablefuture af{
fetchdataasync()};
std::cout << "waiting for data..." << std::endl;
int data = co_await af;
std::cout << "data received: " << data << std::endl;
}
int main() {
asyncfetchdata();
return 0;
}
- 协程与多线程的交互:在多线程环境下,协程可以与线程协作完成任务。可以将协程任务分配到不同的线程中执行,提高并发性能。例如,使用线程池来调度协程任务:
#include
#include
#include
#include
#include
#include
#include
// 线程池类
class threadpool {
public:
threadpool(size_t numthreads) {
for (size_t i = 0; i < numthreads; i) {
threads.emplace_back([this] {
while (true) {
std::function task;
{
std::unique_lock lock(this->queuemutex);
this->condition.wait(lock, [this] {
return !this->tasks.empty() || this->stop; });
if (this->stop && this->tasks.empty())
return;
task = std::move(this->tasks.front());
this->tasks.pop();
}
task();
}
});
}
}
~threadpool() {
{
std::unique_lock lock(queuemutex);
stop = true;
}
condition.notify_all();
for (std::thread &thread : threads) {
thread.join();
}
}
template
void enqueue(f&& f) {
{
std::unique_lock lock(queuemutex);
if (stop)
throw std::runtime_error("enqueue on stopped threadpool");
tasks.emplace(std::forward(f));
}
condition.notify_one();
}
private:
std::vector threads;
std::queue> tasks;
std::mutex queuemutex;
std::condition_variable condition;
bool stop = false;
};
// 协程任务
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 coroutine_task() {
std::cout << "coroutine task started on thread: " << std::this_thread::get_id() << std::endl;
co_await std::suspend_always{
};
std::cout << "coroutine task resumed on thread: " << std::this_thread::get_id() << std::endl;
}
int main() {
threadpool pool(2);
pool.enqueue([] {
auto task = coroutine_task();
// 这里需要手动处理协程的恢复等操作,实际中可能会有更完善的调度机制
});
return 0;
}
(二)优化技巧
- 减少不必要的
co_await
:频繁的协程切换会带来一定的性能损耗,因此要仔细检查代码,避免在不需要异步操作的地方使用co_await
。例如,如果一个函数内部的操作都是同步的,就没必要将其声明为协程。 - 批量处理:如果需要执行大量的异步操作,尽量将它们批量处理,减少协程切换的次数。例如,一次性读取多个文件块,而不是每次读取一个。示例代码如下:
#include
#include
#include
#include
#include
// 模拟异步处理数据的函数
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 process_data_batch(const std::vector& data) {
// 模拟处理数据
for (const auto& line : data) {
std::cout << "processing: " << line << std::endl;
}
co_return;
}
task process_files(const std::vector& filenames) {
const size_t batch_size = 10;
for (const auto& filename : filenames) {
std::ifstream file(filename);
std::vector buffer;
std::string line;
while (std::getline(file, line)) {
buffer.push_back(line);
if (buffer.size() >= batch_size) {
co_await process_data_batch(buffer);
buffer.clear();
}
}
if (!buffer.empty()) {
co_await process_data_batch(buffer);
}
}
}
int main() {
std::vector filenames = {
"file1.txt", "file2.txt"};
auto task = process_files(filenames);
return 0;
}
- 使用高效的调度器:不同的协程库提供了不同的调度器实现,选择一个适合应用场景的调度器,可以显著提升性能。例如,libco库的调度器就非常高效。
- 协程池:如果需要频繁创建和销毁协程,可以考虑使用协程池来复用协程对象,减少内存分配和释放的开销。
- 内存分配优化:协程在执行过程中,可能会频繁地分配和释放小块内存,导致内存碎片,降低内存的利用率。可以采用内存池等技术来优化内存分配,减少内存碎片的产生。
(三)错误处理机制
- 异常处理:在协程中,可以使用
try-catch
块来捕获和处理异常。当协程中抛出异常时,会调用promise_type
的unhandled_exception()
方法。例如:
#include
#include
struct mytask {
struct promise_type {
mytask get_return_object() {
return {
}; }
std::suspend_never initial_suspend() {
return {
}; }
std::suspend_never final_suspend() noexcept {
return {
}; }
void unhandled_exception() {
std::cout << "exception occurred in coroutine." << std::endl;
}
void return_void() {
}
};
};
mytask my_coroutine() {
try {
throw std::runtime_error("an error occurred");
} catch (...) {
throw;
}
co_return;
}
int main() {
auto task = my_coroutine();
return 0;
}
- 错误传播和恢复策略:当协程中出现错误时,需要考虑错误的传播和恢复策略。可以将错误信息传递给调用者,或者在协程内部进行恢复处理。例如,在一个协程链中,如果某个协程出现错误,可以将错误信息返回给上一级协程进行处理。
(四)调试技巧
- 日志记录:在协程中添加日志记录,输出关键步骤和变量的值,有助于定位问题。可以使用标准库的
std::cout
或者第三方日志库来记录日志。 - 调试工具:使用调试工具(如gdb)来调试协程代码。可以设置断点,单步执行代码,查看变量的值和协程的状态。
- 代码审查:仔细审查协程代码,检查是否存在逻辑错误、资源泄漏等问题。特别是在处理协程的生命周期和异常处理时,要确保代码的正确性。
综上所述,c 协程是一种强大的异步编程工具,通过深入学习其入门和精通知识,可以更好地利用协程来提高代码的性能和可维护性。