Aria
3.2.6
C++23 MVVM framework — 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
// Allocation and value-move failures propagate to the awaiting coroutine.
33
// A failed delivery retains any queued waiter so it can be retried or closed;
34
// a throwing move may leave its source value moved-from (basic guarantee).
35
36
#include "
aria/async/task.hpp
"
37
38
#include <coroutine>
39
#include <cstddef>
40
#include <deque>
41
#include <mutex>
42
#include <optional>
43
#include <utility>
44
45
namespace
aria::async
{
46
47
template
<
typename
T>
48
class
Channel
{
49
public
:
50
explicit
Channel
(std::size_t capacity = std::size_t(-1)) : cap_(capacity) {}
51
52
Channel
(
const
Channel
&) =
delete
;
53
Channel
&
operator=
(
const
Channel
&) =
delete
;
54
Channel
(
Channel
&&) =
delete
;
55
Channel
&
operator=
(
Channel
&&) =
delete
;
56
69
~Channel
() {
70
try
{
71
close
();
72
}
catch
(...) {
73
// Resuming a waiter must not throw out of a destructor.
74
}
75
}
76
77
// ── send ──────────────────────────────────────────────────
78
auto
send
(T value) {
79
struct
Awaiter {
80
Channel
* self;
81
T value;
82
83
bool
await_ready()
const
noexcept
{
return
false
; }
84
85
// Reserve delivery for a waiting receiver before considering
86
// the buffer. Publish its value before resuming outside the lock.
87
bool
await_suspend(std::coroutine_handle<> h) {
88
std::coroutine_handle<> wake;
89
bool
suspend =
false
;
90
{
91
std::lock_guard lk(self->mu_);
92
if
(self->closed_) {
93
// Drop the value; just continue.
94
return
false
;
95
}
96
if
(!self->recv_waiters_.empty()) {
97
auto
receiver = self->recv_waiters_.front();
98
receiver.value->emplace(std::move(value));
99
self->recv_waiters_.pop_front();
100
wake = receiver.handle;
101
}
else
if
(self->buffer_.size() < self->cap_) {
102
self->buffer_.push_back(std::move(value));
103
}
else
{
104
self->send_waiters_.push_back({h, &value});
105
suspend =
true
;
106
}
107
}
108
if
(wake) wake.resume();
// outside the lock
109
return
suspend;
110
}
111
112
void
await_resume()
noexcept
{}
113
};
114
return
Awaiter{
this
, std::move(value)};
115
}
116
117
// ── recv ──────────────────────────────────────────────────
118
//
119
// Lost-wakeup safety
120
// ------------------
121
// The decision "is there a value right now?" and "register myself as a
122
// waiter" MUST be atomic with respect to a concurrent `send()` / `close()`.
123
// An earlier design consumed the value in `await_ready` (taking the lock)
124
// and, if empty, registered the waiter in a SEPARATE `await_suspend` lock
125
// acquisition. Between those two locks a `send()` could push to the
126
// buffer, find `recv_waiters_` still empty, and return WITHOUT waking
127
// anyone — the receiver then parked forever (until the next send/close).
128
//
129
// `await_ready` always returns false. The authoritative
130
// "consume-or-suspend" decision is made entirely inside
131
// `await_suspend` under a single lock hold: if a value/closed state is
132
// observable there, we consume it and return `false` (resume without
133
// parking); otherwise we register the waiter and return `true`. There is
134
// no longer any gap between the empty-check and the registration.
135
auto
recv
() {
136
struct
Awaiter {
137
Channel
* self;
138
std::optional<T> value{};
139
140
// Always go through await_suspend so the consume/register
141
// decision is made atomically under one lock. Returning false
142
// here keeps the awaiter cheap; the real work is in
143
// await_suspend.
144
bool
await_ready()
noexcept
{
return
false
; }
145
146
// Returns false (do not suspend) if a value was consumed or the
147
// channel is closed; true (suspend) once the handle is registered.
148
bool
await_suspend(std::coroutine_handle<> h) {
149
std::coroutine_handle<> wake;
150
bool
suspend;
151
{
152
std::lock_guard lk(self->mu_);
153
if
(!self->buffer_.empty()) {
154
value.emplace(std::move(self->buffer_.front()));
155
self->buffer_.pop_front();
156
// A blocked sender can now deposit its value.
157
if
(!self->send_waiters_.empty()) {
158
const
auto
w = self->send_waiters_.front();
159
self->buffer_.push_back(std::move(*w.value));
160
self->send_waiters_.pop_front();
161
wake = w.handle;
162
}
163
suspend =
false
;
164
}
else
if
(!self->send_waiters_.empty()) {
165
// Direct rendezvous, including capacity zero.
166
const
auto
sender = self->send_waiters_.front();
167
value.emplace(std::move(*sender.value));
168
self->send_waiters_.pop_front();
169
wake = sender.handle;
170
suspend =
false
;
171
}
else
if
(self->closed_) {
172
// value stays empty → resume with std::nullopt.
173
suspend =
false
;
174
}
else
{
175
// Genuinely empty and open: register atomically.
176
self->recv_waiters_.push_back({h, &value});
177
suspend =
true
;
178
}
179
}
180
if
(wake) wake.resume();
// outside the lock
181
return
suspend;
182
}
183
184
std::optional<T> await_resume() {
185
// This receive owns its value before it is resumed. Never
186
// race another consumer for a value in the shared buffer.
187
return
std::move(value);
188
}
189
};
190
return
Awaiter{
this
, std::nullopt};
191
}
192
203
void
close
() {
204
std::deque<PendingReceive> receivers;
205
std::deque<PendingSend> senders;
206
{
207
std::lock_guard lk(mu_);
208
closed_ =
true
;
209
receivers.swap(recv_waiters_);
210
senders.swap(send_waiters_);
211
}
212
for
(
auto
receiver : receivers)
213
receiver.handle.resume();
214
for
(
auto
& sender : senders)
215
sender.handle.resume();
216
// Each resumed sender releases its own pending value outside the mutex.
217
}
218
219
[[nodiscard]]
bool
is_closed
()
const
{
220
std::lock_guard lk(mu_);
221
return
closed_;
222
}
223
224
[[nodiscard]] std::size_t
size
()
const
{
225
std::lock_guard lk(mu_);
226
return
buffer_.size();
227
}
228
229
private
:
230
struct
PendingReceive {
231
std::coroutine_handle<> handle;
232
std::optional<T>* value;
233
};
234
235
struct
PendingSend {
236
std::coroutine_handle<> handle;
237
T* value;
238
};
239
240
mutable
std::mutex mu_;
241
std::deque<T> buffer_;
242
std::deque<PendingReceive> recv_waiters_;
243
std::deque<PendingSend> send_waiters_;
244
std::size_t cap_;
245
bool
closed_ =
false
;
246
};
247
248
}
// namespace aria::async
aria::async::Channel::recv
auto recv()
Definition
channel.hpp:135
aria::async::Channel::Channel
Channel(std::size_t capacity=std::size_t(-1))
Definition
channel.hpp:50
aria::async::Channel::~Channel
~Channel()
Releases every still-parked sender / receiver.
Definition
channel.hpp:69
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:78
aria::async::Channel::size
std::size_t size() const
Definition
channel.hpp:224
aria::async::Channel::close
void close()
Mark the channel closed and release EVERY parked coroutine.
Definition
channel.hpp:203
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:219
aria::async
Definition
async_command.hpp:118
task.hpp
modules
async
include
aria
async
channel.hpp
Generated by
1.18.0