之前主要关注的是协程与外部调用者的交互,这次也关注一下对等的协程之间的通信。
实现目标
在C++中实现协程的Channel相对复杂,因为C++标准库中并没有内置的协程间通信机制。C++20引入了协程支持,但主要提供了底层的协程操作,如协程的启动和暂停(通过co_await, co_return, 和 co_yield),并未直接提供Channel或其他高级并发原语。因此,实现一个C++的协程Channel需要依赖C++20的协程功能,结合额外的同步机制,如条件变量、互斥锁和原子操作等。
最终的 Channel 的用例如下:
Task<void, LooperExecutor> Producer(Channel<int> &channel) {
int i = 0;
while (i < 10) {
// 写入时调用 write 函数
co_await channel.write(i++);
// 或者使用 << 运算符
co_await (channel << i++);
}
// 支持关闭
channel.close();
}
Task<void, LooperExecutor> Consumer(Channel<int> &channel) {
while (channel.is_active()) {
try {
// 读取时使用 read 函数,表达式的值就是读取的值
auto received = co_await channel.read();
int received;
// 或者使用 >> 运算符将读取的值写入变量当中
co_await (channel >> received);
} catch (std::exception &e) {
// 捕获 Channel 关闭时抛出的异常
}
}
}
co_await 表达式的支持
想要支持 co_await 表达式,只需要为 Channel 读写函数返回的 Awaiter 类型添加相应的 await_transform 函数。假定Channel的read 和 write 两个函数的返回值类型 ReaderAwaiter 和 WriterAwaiter,接下来就添加一个非常简单的 await_transform 的支持:
template<typename ResultType, typename Executor>
struct TaskPromise {
template<typename _ValueType>
auto await_transform(ReaderAwaiter<_ValueType> reader_awaiter) {
reader_awaiter.executor = &executor;
return reader_awaiter;
}
template<typename _ValueType>
auto await_transform(WriterAwaiter<_ValueType> writer_awaiter) {
writer_awaiter.executor = &executor;
return writer_awaiter;
}
}
由于 Channel 的 buffer 和对 Channel 的读写本身会决定协程是否挂起或恢复,因此这些逻辑都将在 Channel 当中给出,TaskPromise 能做的就是把调度器传过去,当协程恢复时使用。
Awaiter 的实现
Awaiter 负责在挂起时将自己存入 Channel,并且在需要时恢复协程。因此除了前面看到需要在恢复执行协程时的调度器之外,Awaiter 还需要持有 Channel、需要读写的值。
WriterAwaiter
template<typename ValueType>
struct WriterAwaiter {
Channel<ValueType> *channel;
// 调度器不是必须的,如果没有,则直接在当前线程执行(等价于 NoopExecutor)
AbstractExecutor *executor = nullptr;
// 写入 Channel 的值
ValueType _value;
std::coroutine_handle<> handle;
WriterAwaiter(Channel<ValueType> *channel, ValueType value)
: channel(channel), _value(value) {}
bool await_ready() {
return false;
}
auto await_suspend(std::coroutine_handle<> coroutine_handle) {
// 记录协程 handle,恢复时用
this->handle = coroutine_handle;
// 将自身传给 Channel,Channel 内部会根据自身状态处理是否立即恢复或者挂起
channel->try_push_writer(this);
}
void await_resume() {
// Channel 关闭时也会将挂起的读写协程恢复
// 要检查是否是关闭引起的恢复,如果是,check_closed 会抛出 Channel 关闭异常
channel->check_closed();
}
// Channel 当中恢复该协程时调用 resume 函数
void resume() {
// 我们将调度器调度的逻辑封装在这里
if (executor) {
executor->execute([this]() { handle.resume(); });
} else {
handle.resume();
}
}
};
ReaderAwaiter
template<typename ValueType>
struct ReaderAwaiter {
Channel<ValueType> *channel;
AbstractExecutor *executor = nullptr;
ValueType _value;
// 用于 channel >> received; 这种情况
// 需要将变量的地址传入,协程恢复时写入变量内存
ValueType* p_value = nullptr;
std::coroutine_handle<> handle;
explicit ReaderAwaiter(Channel<ValueType> *channel) : channel(channel) {}
bool await_ready() { return false; }
auto await_suspend(std::coroutine_handle<> coroutine_handle) {
this->handle = coroutine_handle;
// 将自身传给 Channel,Channel 内部会根据自身状态处理是否立即恢复或者挂起
channel->try_push_reader(this);
}
int await_resume() {
// Channel 关闭时也会将挂起的读写协程恢复
// 要检查是否是关闭引起的恢复,如果是,check_closed 会抛出 Channel 关闭异常
channel->check_closed();
return _value;
}
// Channel 当中正常恢复读协程时调用 resume 函数
void resume(ValueType value) {
this->_value = value;
if (p_value) {
*p_value = value;
}
resume();
}
// Channel 关闭时调用 resume() 函数来恢复该协程
// 在 await_resume 当中,如果 Channel 关闭,会抛出 Channel 关闭异常
void resume() {
if (executor) {
executor->execute([this]() { handle.resume(); });
} else {
handle.resume();
}
}
};
Awaiter 的功能就是:负责用协程的调度器在需要时恢复协程,处理读写的值的传递(通过Channel)。
Channel 的实现
接下来给出 Channel 当中根据 buffer 的情况来处理读写两端的挂起和恢复的逻辑。
基本结构
template<typename ValueType>
struct Channel {
...
struct ChannelClosedException : std::exception {
const char *what() const noexcept override {
return "Channel is closed.";
}
};
void check_closed() {
// 如果已经关闭,则抛出异常
if (!_is_active.load(std::memory_order_relaxed)) {
throw ChannelClosedException();
}
}
explicit Channel(int capacity = 0) : buffer_capacity(capacity) {
_is_active.store(true, std::memory_order_relaxed);
}
// true 表示 Channel 尚未关闭
bool is_active() {
return _is_active.load(std::memory_order_relaxed);
}
// 关闭 Channel
void close() {
bool expect = true;
// 判断如果已经关闭,则不再重复操作
// 比较 _is_active 为 true 时才会完成设置操作,并且返回 true
if(_is_active.compare_exchange_strong(expect, false, std::memory_order_relaxed)) {
// 清理资源
clean_up();
}
}
// 不希望 Channel 被移动或者复制
Channel(Channel &&channel) = delete;
Channel(Channel &) = delete;
Channel &operator=(Channel &) = delete;
// 销毁时关闭
~Channel() {
close();
}
private:
// buffer 的容量
int buffer_capacity;
std::queue<ValueType> buffer;
// buffer 已满时,新来的写入者需要挂起保存在这里等待恢复
std::list<WriterAwaiter<ValueType> *> writer_list;
// buffer 为空时,新来的读取者需要挂起保存在这里等待恢复
std::list<ReaderAwaiter<ValueType> *> reader_list;
// Channel 的状态标识
std::atomic<bool> _is_active;
std::mutex channel_lock;
std::condition_variable channel_condition;
void clean_up() {
std::lock_guard lock(channel_lock);
// 需要对已经挂起等待的协程予以恢复执行
for (auto writer : writer_list) {
writer->resume();
}
writer_list.clear();
for (auto reader : reader_list) {
reader->resume();
}
reader_list.clear();
// 清空 buffer
decltype(buffer) empty_buffer;
std::swap(buffer, empty_buffer);
}
};
初始化和运行时:
- 通道在创建时是开放的,可以进行数据的读写操作。
- 当数据写入满足或读取可进行时,可能有等待的读写者被恢复执行。
关闭和清理:通道的关闭操作会触发资源的清理,包括清空缓冲区和恢复所有挂起的操作,确保没有线程或协程因通道关闭而无限期等待。
read 和 write
template<typename ValueType>
struct Channel {
auto write(ValueType value) {
check_closed();
return WriterAwaiter<ValueType>(this, value);
}
auto operator<<(ValueType value) {
return write(value);
}
auto read() {
check_closed();
return ReaderAwaiter<ValueType>(this);
}
auto operator>>(ValueType &value_ref) {
auto awaiter = read();
// 保存待赋值的变量的地址,方便后续写入
awaiter.p_value = &value_ref;
return awaiter;
}
}
write 方法:
- 这个方法首先调用 check_closed() 检查通道是否已关闭。如果通道关闭,则会抛出 ChannelClosedException。
- 若通道未关闭,方法将创建一个 WriterAwaiter 对象,这个对象负责管理写操作的挂起和恢复。WriterAwaiter 构造时接收通道自身的指针和要写入的值。
read 方法:
- 类似于 write,read 方法首先检查通道是否已关闭,如果关闭,则抛出异常。
- 如果通道开启,则创建并返回一个 ReaderAwaiter 对象,这个对象负责管理读操作的挂起和恢复。
这些对象会在协程尝试进行不可能立即完成的操作(如写入一个满的缓冲区或从空的缓冲区读取)时挂起协程。当操作变得可行时(如缓冲区有空间可写或有数据可读),相关的 Awaiter 会恢复协程的执行。
try_push_writer 和 try_push_reader
try_push_writer 调用时,意味着有一个新的写入者挂起准备写入值到 Channel 当中,这时候有以下几种情况:
- Channel 当中有挂起的读取者,写入者直接将要写入的值传给读取者,恢复读取者,恢复写入者。
- Channel 的 buffer 没满,写入者把值写入 buffer,然后立即恢复执行。
- Channel 的 buffer 已满,则写入者被存入挂起列表(writer_list)等待新的读取者读取时再恢复。
void try_push_writer(WriterAwaiter<ValueType> *writer_awaiter) {
std::unique_lock lock(channel_lock);
check_closed();
// 检查有没有挂起的读取者,对应情况 1
if (!reader_list.empty()) {
auto reader = reader_list.front();
reader_list.pop_front();
lock.unlock();
reader->resume(writer_awaiter->_value);
writer_awaiter->resume();
return;
}
// buffer 未满,对应情况 2
if (buffer.size() < buffer_capacity) {
buffer.push(writer_awaiter->_value);
lock.unlock();
writer_awaiter->resume();
return;
}
// buffer 已满,对应情况 3
writer_list.push_back(writer_awaiter);
}
相对应的,try_push_reader 调用时,意味着有一个新的读取者挂起准备从 Channel 当中读取值,这时候有以下几种情况:
- Channel 的 buffer 非空,读取者从 buffer 当中读取值,如果此时有挂起的写入者,需要去队头的写入者将值写入 buffer,然后立即恢复该写入者和当次的读取者。
- Channel 当中有挂起的写入者,写入者直接将要写入的值传给读取者,恢复读取者,恢复写入者
- Channel 的 buffer 为空,则读取者被存入挂起列表(reader_list)等待新的写入者写入时再恢复。
void try_push_reader(ReaderAwaiter<ValueType> *reader_awaiter) {
std::unique_lock lock(channel_lock);
check_closed();
// buffer 非空,对应情况 1
if (!buffer.empty()) {
auto value = buffer.front();
buffer.pop();
if (!writer_list.empty()) {
// 有挂起的写入者要及时将其写入 buffer 并恢复执行
auto writer = writer_list.front();
writer_list.pop_front();
buffer.push(writer->_value);
lock.unlock();
writer->resume();
} else {
lock.unlock();
}
reader_awaiter->resume(value);
return;
}
// 有写入者挂起,对应情况 2
if (!writer_list.empty()) {
auto writer = writer_list.front();
writer_list.pop_front();
lock.unlock();
reader_awaiter->resume(writer->_value);
writer->resume();
return;
}
// buffer 为空,对应情况 3
reader_list.push_back(reader_awaiter);
}
监听协程的提前销毁
观察上述代码,Channel 对象必须在持有 Channel 实例的协程退出之前关闭。在 Channel 当中持有了已经挂起的读写协程的 Awaiter 的指针,一旦协程销毁,这些 Awaiter 也会被销毁,Channel 在关闭时试图恢复这些读写协程时就会出现程序崩溃(访问了野指针)。
为了解决这个问题,需要在 Awaiter 销毁时主动将自己的指针从 Channel 当中移除。
template<typename ValueType>
struct ReaderAwaiter {
ReaderAwaiter(ReaderAwaiter&& other) noexcept
: channel(std::exchange(other.channel, nullptr)),
executor(std::exchange(other.executor, nullptr)),
_value(other._value),
p_value(std::exchange(other.p_value, nullptr)),
handle(other.handle) {}
int await_resume() {
auto channel = this->channel;
this->channel = nullptr;
channel->check_closed();
return _value;
}
~ReaderAwaiter() {
if (channel) channel->remove_reader(this);
}
}
实现了移动构造函数,ReaderAwaiter在被移动后会将原对象的channel指针置为nullptr。原来的Awaiter对象不再与任何Channel关联,从而防止在原Awaiter对象被销毁时误操作已移走的资源。
协程恢复时将自身持有的channel指针置空。这是因为当协程由于await表达式被挂起后恢复执行时,await_resume()被调用以继续执行协程。将channel设置为nullptr之后,如果在后续的执行中再次错误地或意外地引用了channel,这将直接导致访问空指针错误而非进行无效或危险的操作。
在ReaderAwaiter的析构函数中,如果其channel成员变量仍然非空,表明该Awaiter可能在协程尚未恢复执行前被销毁(例如协程的异常退出或提前结束)。在这种情况下,Awaiter负责通知Channel从其等待列表中移除自己,确保Channel不会在未来尝试访问已经销毁的Awaiter。
对应的,Channel 当中也需要增加 remove_reader 函数:
template<typename ValueType>
struct Channel {
void remove_reader(ReaderAwaiter<ValueType> *reader_awaiter) {
// 并发环境,修改 reader_list 的操作都需要加锁
std::lock_guard lock(channel_lock);
reader_list.remove(reader_awaiter);
}
}
WriterAwaiter 的修改类似,之后即使把正在等待读写 Channel 的协程提前结束销毁,也不会影响 Channel 的继续使用以及后续的正常关闭了。
结果展示
测试代码如下所示
Task<void, LooperExecutor> Producer(Channel<int>& channel) {
int i = 0;
while (i < 10) {
debug("send: ", i);
co_await(channel << i++);
co_await 50ms;
}
co_await 5s;
channel.close();
debug("close channel, exit.");
}
Task<void, LooperExecutor> Consumer(Channel<int>& channel) {
while (channel.is_active()) {
try {
int received;
co_await(channel >> received);
debug("receive: ", received);
co_await 500ms;
}
catch (std::exception& e) {
//
}
}
debug("exit.");
}
Task<void, LooperExecutor> Consumer2(Channel<int>& channel) {
while (channel.is_active()) {
try {
auto received = co_await channel.read();
debug("receive2: ", received);
co_await 300ms;
}
catch (std::exception& e) {
//
}
}
debug("exit.");
}
// co_wait 时间也会有run_loop exit.
void test_channel() {
debug("test_channel()");
auto channel = Channel<int>(5);
auto producer = Producer(channel);
auto consumer = Consumer(channel);
auto consumer2 = Consumer2(channel);
std::this_thread::sleep_for(10s);
}
int main() {
test_channel();
return 0;
}
完整代码见个人github的Coroutines项目。
Current time: 19:47.300 18784 send: 0
Current time: 19:47.300 26408 receive2: 0
Current time: 19:47.356 18784 send: 1
Current time: 19:47.357 38656 receive: 1
Current time: 19:47.419 18784 send: 2
Current time: 19:47.482 18784 send: 3
Current time: 19:47.545 18784 send: 4
Current time: 19:47.607 18784 send: 5
Current time: 19:47.607 26408 receive2: 2
Current time: 19:47.669 18784 send: 6
Current time: 19:47.731 18784 send: 7
Current time: 19:47.791 18784 send: 8
Current time: 19:47.869 38656 receive: 3
Current time: 19:47.915 26408 receive2: 4
Current time: 19:47.931 18784 send: 9
Current time: 19:48.224 26408 receive2: 5
Current time: 19:48.379 38656 receive: 6
Current time: 19:48.532 26408 receive2: 7
Current time: 19:48.839 26408 receive2: 8
Current time: 19:48.886 38656 receive: 9
基本符合其中的等待时间和处理逻辑。