推荐文章:
https://lewissbaker.github.io/2017/09/25/coroutine-theory
是看到上面这篇文章决定简单记录一下,他写的这个描述非常准确而且清晰:普通函数有两个操作,执行和返回。在执行的时候,会创建一个函数帧,同时暂停原函数,在执行返回操作之后恢复原函数。
这句话就有点醒到我,暂停函数的概念不是协程才有,函数本身就有的。或者进一步说,协程是泛化的函数,函数是限定了协程在调用时暂停原函数,返回时恢复原函数;而协程是可以在调用时选择暂停原函数还是新函数,同时可以在函数执行中被暂停挂起,甚至可以调度之前被暂停的其他协程。
当然二者还是有差别的,因为函数是存在帧栈的,但是其实细想,帧栈的概念也是为了简化开发者而实现的,为了能够找回调用本函数的函数,以及传递数据给调用函数。
但是为什么一定要返回原函数呢?只是为了简化问题,函数本身就是一个功能,一个行为,一个动作,我们通常要等这个函数执行完成获取某些参数才进行下一个函数。
比如我如果有一个调用f(a(), b())
,我们通常的执行顺序是执行完a()函数之后返回这个上下文再执行b()函数然后再将栈上两个的结果执行f函数,但是为什么a()执行完成之后还需要返回这个上下文呢?
只是基于同步原语的模型下,这个逻辑非常的简单。
但是我们也完全可以不用帧栈,直接跳转执行a()的函数,完成之后不跳转会上一层,而是直接执行b(),然后最终调度执行f()。
但是在这种模型之下,调度和数据传输就会成为新的问题。
C++20的协程就看穿了这个本质,作为一种更抽象的函数,它通过引入无栈协程实现了一种更加抽象的函数,每个函数是一个可以独立被调度的功能,一个协程可以被暂停几次是基于这种模型的“额外功能”。
但是这个模型下解决的最重要的问题是,将同步原语彻底干掉了,函数调用是一个独立的过程,执行完之后不会自动返回上一层帧栈,而是执行另外一段逻辑,在这一段逻辑里可以决定再继续恢复哪个协程,
因为不用返回上一层帧栈,所以在这个函数调用模型下,栈的概念就被干掉了,同时这样的概念下,函数就不存在返回值了,被调用函数返回不一定会回到它的调用者那里,因此a()的值就不是返回值,而是这个需要被调度的协程本身。
因此f(a(), b())
是不被允许的,因为这个调用是包含了f的调用需要等待a(), b()完成之后,将结果拿出来再作为f的参数。
因此在无栈协程概念下这段伪代码应该这么写:
coroutine_a = launch(a())
coroutine_b = launch(b())
co_await (coroutine_a, coroutine_b)
f(coroutine_a.get_a_result(), coroutine_b.get_b_result())
如果能看懂这个例程,你应该就能完全理解我心中所想的抽象概念,以及C++20协程的抽象根本。
然后再回到C++20的协程本身,网上的教程和demo很多了,不过我还是想吐槽一下,抽象程度太高导致C++20原本的协程真的很不好用,std::coroutine_traits_base
, awaitable
, promise_type
, 这几个概念真的很抽象。
但是这才是C++啊,极致的性能极致的抽象,更何况asio还有很优秀的抽象:
//
// echo_server.cpp
// ~~~~~~~~~~~~~~~
//
// Copyright (c) 2003-2024 Christopher M. Kohlhoff (chris at kohlhoff dot com)
//
// Distributed under the Boost Software License, Version 1.0. (See accompanying
// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
//
#include <asio/co_spawn.hpp>
#include <asio/detached.hpp>
#include <asio/io_context.hpp>
#include <asio/ip/tcp.hpp>
#include <asio/signal_set.hpp>
#include <asio/write.hpp>
#include <cstdio>
using asio::ip::tcp;
using asio::awaitable;
using asio::co_spawn;
using asio::detached;
using asio::use_awaitable;
namespace this_coro = asio::this_coro;
#if defined(ASIO_ENABLE_HANDLER_TRACKING)
# define use_awaitable \
asio::use_awaitable_t(__FILE__, __LINE__, __PRETTY_FUNCTION__)
#endif
awaitable<void> echo(tcp::socket socket)
{
try
{
char data[1024];
for (;;)
{
std::size_t n = co_await socket.async_read_some(asio::buffer(data), use_awaitable);
co_await async_write(socket, asio::buffer(data, n), use_awaitable);
}
}
catch (std::exception& e)
{
std::printf("echo Exception: %s\n", e.what());
}
}
awaitable<void> listener()
{
auto executor = co_await this_coro::executor;
tcp::acceptor acceptor(executor, {tcp::v4(), 55555});
for (;;)
{
tcp::socket socket = co_await acceptor.async_accept(use_awaitable);
co_spawn(executor, echo(std::move(socket)), detached);
}
}
int main()
{
try
{
asio::io_context io_context(1);
asio::signal_set signals(io_context, SIGINT, SIGTERM);
signals.async_wait([&](auto, auto){ io_context.stop(); });
co_spawn(io_context, listener(), detached);
io_context.run();
}
catch (std::exception& e)
{
std::printf("Exception: %s\n", e.what());
}
}
End。