Aria 2.0.0
C++23 MVVM framework (C++20 minimum) — 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
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
42namespace aria::async {
43
44template<typename T>
45class Channel {
46public:
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;
53
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
226private:
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
auto recv()
Definition channel.hpp:132
Channel(std::size_t capacity=std::size_t(-1))
Definition channel.hpp:47
~Channel()
Releases every still-parked sender / receiver.
Definition channel.hpp:66
Channel & operator=(Channel &&)=delete
Channel & operator=(const Channel &)=delete
auto send(T value)
Definition channel.hpp:75
std::size_t size() const
Definition channel.hpp:221
void close()
Mark the channel closed and release EVERY parked coroutine.
Definition channel.hpp:200
Channel(Channel &&)=delete
Channel(const Channel &)=delete
bool is_closed() const
Definition channel.hpp:216
Definition async_command.hpp:118