Aria
2.0.0
C++23 MVVM framework (C++20 minimum) — reactive, coroutine-first, ABI-layered
Toggle main menu visibility
Loading...
Searching...
No Matches
channel.hpp
Go to the documentation of this file.
1
#pragma once
2
3
// Channel<T>: bounded async queue between coroutines.
4
//
5
// Channel<int> ch{capacity = 4};
6
//
7
// // Producer
8
// Task<void> producer() {
9
// for (int i = 0; i < 10; ++i) co_await ch.send(i);
10
// ch.close();
11
// }
12
//
13
// // Consumer
14
// Task<void> consumer() {
15
// while (auto v = co_await ch.recv()) {
16
// std::cout << *v << '\n';
17
// }
18
// }
19
//
20
// `send` suspends if the buffer is full and no receiver is waiting; `recv`
21
// suspends if neither a buffered value nor a sender is available. Capacity
22
// zero is a rendezvous: each sender waits for a receiver (or vice versa).
23
// `close()` wakes ALL pending waiters: unassigned receivers observe
24
// `std::nullopt` (end of stream), and parked senders are released with their
25
// pending value dropped. Buffered and already assigned values remain readable.
26
//
27
// All resumes happen *outside* the internal mutex to avoid recursive locks.
28
// Multiple producers and consumers may use the channel concurrently. The
29
// channel must outlive concurrent method calls, and parked coroutine frames
30
// must remain alive until resumed. Destroying a suspended Task is not a
31
// cancellation operation.
32
33
#include "
aria/async/task.hpp
"
34
35
#include <coroutine>
36
#include <cstddef>
37
#include <deque>
38
#include <mutex>
39
#include <optional>
40
#include <utility>
41
42
namespace
aria::async
{
43
44
template
<
typename
T>
45
class
Channel
{
46
public
:
47
explicit
Channel
(std::size_t capacity = std::size_t(-1)) : cap_(capacity) {}
48
49
Channel
(
const
Channel
&) =
delete
;
50
Channel
&
operator=
(
const
Channel
&) =
delete
;
51
Channel
(
Channel
&&) =
delete
;
52
Channel
&
operator=
(
Channel
&&) =
delete
;
53
66
~Channel
() {
67
try
{
68
close
();
69
}
catch
(...) {
70
// Resuming a waiter must not throw out of a destructor.
71
}
72
}
73
74
// ── send ──────────────────────────────────────────────────
75
auto
send
(T value) {
76
struct
Awaiter {
77
Channel
* self;
78
T value;
79
80
bool
await_ready()
const
noexcept
{
return
false
; }
81
82
// Reserve delivery for a waiting receiver before considering
83
// the buffer. Publish its value before resuming outside the lock.
84
bool
await_suspend(std::coroutine_handle<> h)
noexcept
{
85
std::coroutine_handle<> wake;
86
bool
suspend =
false
;
87
{
88
std::lock_guard lk(self->mu_);
89
if
(self->closed_) {
90
// Drop the value; just continue.
91
return
false
;
92
}
93
if
(!self->recv_waiters_.empty()) {
94
auto
receiver = self->recv_waiters_.front();
95
receiver.value->emplace(std::move(value));
96
self->recv_waiters_.pop_front();
97
wake = receiver.handle;
98
}
else
if
(self->buffer_.size() < self->cap_) {
99
self->buffer_.push_back(std::move(value));
100
}
else
{
101
self->send_waiters_.push_back({h, std::move(value)});
102
suspend =
true
;
103
}
104
}
105
if
(wake) wake.resume();
// outside the lock
106
return
suspend;
107
}
108
109
void
await_resume()
noexcept
{}
110
};
111
return
Awaiter{
this
, std::move(value)};
112
}
113
114
// ── recv ──────────────────────────────────────────────────
115
//
116
// Lost-wakeup safety
117
// ------------------
118
// The decision "is there a value right now?" and "register myself as a
119
// waiter" MUST be atomic with respect to a concurrent `send()` / `close()`.
120
// An earlier design consumed the value in `await_ready` (taking the lock)
121
// and, if empty, registered the waiter in a SEPARATE `await_suspend` lock
122
// acquisition. Between those two locks a `send()` could push to the
123
// buffer, find `recv_waiters_` still empty, and return WITHOUT waking
124
// anyone — the receiver then parked forever (until the next send/close).
125
//
126
// `await_ready` always returns false. The authoritative
127
// "consume-or-suspend" decision is made entirely inside
128
// `await_suspend` under a single lock hold: if a value/closed state is
129
// observable there, we consume it and return `false` (resume without
130
// parking); otherwise we register the waiter and return `true`. There is
131
// no longer any gap between the empty-check and the registration.
132
auto
recv
() {
133
struct
Awaiter {
134
Channel
* self;
135
std::optional<T> value{};
136
137
// Always go through await_suspend so the consume/register
138
// decision is made atomically under one lock. Returning false
139
// here keeps the awaiter cheap; the real work is in
140
// await_suspend.
141
bool
await_ready()
noexcept
{
return
false
; }
142
143
// Returns false (do not suspend) if a value was consumed or the
144
// channel is closed; true (suspend) once the handle is registered.
145
bool
await_suspend(std::coroutine_handle<> h)
noexcept
{
146
std::coroutine_handle<> wake;
147
bool
suspend;
148
{
149
std::lock_guard lk(self->mu_);
150
if
(!self->buffer_.empty()) {
151
value.emplace(std::move(self->buffer_.front()));
152
self->buffer_.pop_front();
153
// A blocked sender can now deposit its value.
154
if
(!self->send_waiters_.empty()) {
155
auto
w = std::move(self->send_waiters_.front());
156
self->send_waiters_.pop_front();
157
self->buffer_.push_back(std::move(w.value));
158
wake = w.handle;
159
}
160
suspend =
false
;
161
}
else
if
(!self->send_waiters_.empty()) {
162
// Direct rendezvous, including capacity zero.
163
auto
sender = std::move(self->send_waiters_.front());
164
self->send_waiters_.pop_front();
165
value.emplace(std::move(sender.value));
166
wake = sender.handle;
167
suspend =
false
;
168
}
else
if
(self->closed_) {
169
// value stays empty → resume with std::nullopt.
170
suspend =
false
;
171
}
else
{
172
// Genuinely empty and open: register atomically.
173
self->recv_waiters_.push_back({h, &value});
174
suspend =
true
;
175
}
176
}
177
if
(wake) wake.resume();
// outside the lock
178
return
suspend;
179
}
180
181
std::optional<T> await_resume()
noexcept
{
182
// This receive owns its value before it is resumed. Never
183
// race another consumer for a value in the shared buffer.
184
return
std::move(value);
185
}
186
};
187
return
Awaiter{
this
, std::nullopt};
188
}
189
200
void
close
() {
201
std::deque<PendingReceive> receivers;
202
std::deque<PendingSend> senders;
203
{
204
std::lock_guard lk(mu_);
205
closed_ =
true
;
206
receivers.swap(recv_waiters_);
207
senders.swap(send_waiters_);
208
}
209
for
(
auto
receiver : receivers)
210
receiver.handle.resume();
211
for
(
auto
& sender : senders)
212
sender.handle.resume();
213
// Pending send values are destroyed here, outside the channel mutex.
214
}
215
216
[[nodiscard]]
bool
is_closed
()
const
{
217
std::lock_guard lk(mu_);
218
return
closed_;
219
}
220
221
[[nodiscard]] std::size_t
size
()
const
{
222
std::lock_guard lk(mu_);
223
return
buffer_.size();
224
}
225
226
private
:
227
struct
PendingReceive {
228
std::coroutine_handle<> handle;
229
std::optional<T>* value;
230
};
231
232
struct
PendingSend {
233
std::coroutine_handle<> handle;
234
T value;
235
};
236
237
mutable
std::mutex mu_;
238
std::deque<T> buffer_;
239
std::deque<PendingReceive> recv_waiters_;
240
std::deque<PendingSend> send_waiters_;
241
std::size_t cap_;
242
bool
closed_ =
false
;
243
};
244
245
}
// namespace aria::async
aria::async::Channel::recv
auto recv()
Definition
channel.hpp:132
aria::async::Channel::Channel
Channel(std::size_t capacity=std::size_t(-1))
Definition
channel.hpp:47
aria::async::Channel::~Channel
~Channel()
Releases every still-parked sender / receiver.
Definition
channel.hpp:66
aria::async::Channel::operator=
Channel & operator=(Channel &&)=delete
aria::async::Channel::operator=
Channel & operator=(const Channel &)=delete
aria::async::Channel::send
auto send(T value)
Definition
channel.hpp:75
aria::async::Channel::size
std::size_t size() const
Definition
channel.hpp:221
aria::async::Channel::close
void close()
Mark the channel closed and release EVERY parked coroutine.
Definition
channel.hpp:200
aria::async::Channel::Channel
Channel(Channel &&)=delete
aria::async::Channel::Channel
Channel(const Channel &)=delete
aria::async::Channel::is_closed
bool is_closed() const
Definition
channel.hpp:216
aria::async
Definition
async_command.hpp:118
task.hpp
modules
async
include
aria
async
channel.hpp
Generated by
1.18.0