Aria 3.2.6
C++23 MVVM framework — reactive, coroutine-first, ABI-layered
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
45namespace aria::async {
46
47template<typename T>
48class Channel {
49public:
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;
56
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
229private:
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
auto recv()
Definition channel.hpp:135
Channel(std::size_t capacity=std::size_t(-1))
Definition channel.hpp:50
~Channel()
Releases every still-parked sender / receiver.
Definition channel.hpp:69
Channel & operator=(Channel &&)=delete
Channel & operator=(const Channel &)=delete
auto send(T value)
Definition channel.hpp:78
std::size_t size() const
Definition channel.hpp:224
void close()
Mark the channel closed and release EVERY parked coroutine.
Definition channel.hpp:203
Channel(Channel &&)=delete
Channel(const Channel &)=delete
bool is_closed() const
Definition channel.hpp:219
Definition async_command.hpp:118