|
1 | 1 | #include <engine/impl/async_flat_combining_queue.hpp> |
2 | 2 |
|
3 | | -#include <functional> |
4 | | - |
5 | 3 | #include <engine/task/task_context.hpp> |
6 | 4 | #include <userver/utils/assert.hpp> |
7 | 5 |
|
8 | 6 | USERVER_NAMESPACE_BEGIN |
9 | 7 |
|
10 | 8 | namespace engine::impl { |
11 | 9 |
|
12 | | -// The consumer is not allowed to leave before being notified, thus this is only a WeakAwaitable. |
13 | | -template <auto TryStartWaiting> |
14 | | -class AsyncFlatCombiningQueue::Awaitable final : public impl::WeakAwaitable { |
15 | | -public: |
16 | | - explicit Awaitable(AsyncFlatCombiningQueue& queue) |
17 | | - : queue_(queue) |
18 | | - {} |
19 | | - |
20 | | - bool IsReady() const noexcept override { return false; } |
21 | | - |
22 | | - void TryAppendAwaiter(boost::intrusive_ptr<Awaiter>& awaiter, [[maybe_unused]] std::uintptr_t context) override { |
23 | | - if (std::invoke(TryStartWaiting, queue_)) { |
24 | | - // We committed to sleeping and will be woken up only via |
25 | | - // NotifyAsyncConsumer. No deadlines or cancellations are allowed, |
26 | | - // otherwise another thread or task may notify us later and wake up |
27 | | - // a dead task. |
28 | | - awaiter = nullptr; |
29 | | - } |
30 | | - } |
31 | | - |
32 | | - boost::intrusive_ptr<Awaiter> RemoveAwaiter( |
33 | | - [[maybe_unused]] Awaiter& awaiter, |
34 | | - [[maybe_unused]] std::uintptr_t context |
35 | | - ) noexcept override { |
36 | | - // The notification happened through consuming_task_context_, but we'll pretend that the awaitable did it. |
37 | | - // We won't be notified anymore, since we are the sole consumer now. |
38 | | - return nullptr; |
39 | | - } |
40 | | - |
41 | | -private: |
42 | | - AsyncFlatCombiningQueue& queue_; |
43 | | -}; |
44 | | - |
45 | 10 | AsyncFlatCombiningQueue::Consumer::Consumer(AsyncFlatCombiningQueue& queue) |
46 | 11 | : queue_(&queue) |
47 | 12 | { |
@@ -78,22 +43,30 @@ AsyncFlatCombiningQueue::AsyncFlatCombiningQueue() { queue_.Push(consumer_node_) |
78 | 43 | AsyncFlatCombiningQueue::~AsyncFlatCombiningQueue() { |
79 | 44 | UASSERT(!DoTryPop()); |
80 | 45 | UASSERT(!has_consumer_); |
81 | | - UASSERT(!has_waiter_); |
82 | 46 | } |
83 | 47 |
|
84 | 48 | auto AsyncFlatCombiningQueue::WaitAndStartConsuming() -> Consumer { |
85 | | - Wait<&AsyncFlatCombiningQueue::TryStartWaitingForConsumer>(); |
86 | | - // The async consumer role is acquired; it is now working. |
87 | | - notification_state_->store(NotificationState::kWorking); |
| 49 | + if (TryStartWaitingForConsumer()) { |
| 50 | + // No deadlines or cancellations are allowed here, otherwise this task may |
| 51 | + // walk away, and DoTryStopConsuming will try to hand over the consumer |
| 52 | + // role to nobody. |
| 53 | + UASSERT(!engine::current_task::GetCurrentTaskContext().IsCancellable()); |
| 54 | + [[maybe_unused]] const bool success = start_consuming_event_.WaitForEvent(); |
| 55 | + UASSERT(success); |
| 56 | + } |
| 57 | + // The async consumer role is acquired. |
88 | 58 | return Consumer{*this}; |
89 | 59 | } |
90 | 60 |
|
91 | 61 | void AsyncFlatCombiningQueue::WaitWhileEmpty(Consumer& consumer) noexcept { |
92 | 62 | UASSERT(consumer.queue_ == this); |
93 | | - Wait<&AsyncFlatCombiningQueue::TryStartWaitingWhileEmpty>(); |
94 | | - // Either we were woken up after sleeping, or we were already notified and did |
95 | | - // not sleep. In both cases we are working again. |
96 | | - notification_state_->store(NotificationState::kWorking); |
| 63 | + // No deadlines or cancellations are allowed here, otherwise this task may |
| 64 | + // walk away while still being registered as the queue's consumer. |
| 65 | + UASSERT(!engine::current_task::GetCurrentTaskContext().IsCancellable()); |
| 66 | + // Auto-resets on success, guaranteeing an 'acquire' on the signal, so that |
| 67 | + // the newly pushed nodes are visible to the subsequent DoTryPop calls. |
| 68 | + [[maybe_unused]] const bool success = empty_queue_event_->WaitForEvent(); |
| 69 | + UASSERT(success); |
97 | 70 | } |
98 | 71 |
|
99 | 72 | AsyncFlatCombiningQueue::NodeBase* AsyncFlatCombiningQueue::DoTryPop() noexcept { |
@@ -123,63 +96,22 @@ bool AsyncFlatCombiningQueue::DoTryStopConsuming() noexcept { |
123 | 96 | UASSERT(has_consumer_.exchange(false)); |
124 | 97 | if (std::exchange(should_pass_consumer_to_waiter_, false)) { |
125 | 98 | // The waiter will consume the remaining nodes. |
126 | | - notification_state_->store(NotificationState::kWorkingNotified); |
127 | | - NotifyAsyncConsumer(); |
| 99 | + start_consuming_event_.Send(); |
128 | 100 | return true; |
129 | 101 | } else if (queue_.PushIfEmpty(consumer_node_)) { |
130 | 102 | // There is no async consumer anymore. |
131 | | - notification_state_->store(NotificationState::kWorkingNotified); |
132 | 103 | return true; |
133 | 104 | } else { |
134 | 105 | UASSERT(!has_consumer_.exchange(true)); |
135 | 106 | return false; |
136 | 107 | } |
137 | 108 | } |
138 | 109 |
|
139 | | -void AsyncFlatCombiningQueue::NotifyAsyncConsumer() noexcept { |
140 | | - UASSERT(consuming_task_context_); |
141 | | - TaskContext::Wakeup( |
142 | | - boost::intrusive_ptr<TaskContext>{consuming_task_context_}, |
143 | | - TaskContext::WakeupSource::kNotify, |
144 | | - NoEpoch{} |
145 | | - ); |
146 | | -} |
147 | | - |
148 | 110 | void AsyncFlatCombiningQueue::NotifyAsyncConsumerIfSleeping() noexcept { |
149 | | - // If the consumer is sleeping, this wins the race against its CAS to |
150 | | - // kSleeping and we wake it up. If it is (already) working, it will re-check |
151 | | - // the queue before sleeping again, so no wakeup is needed. |
152 | | - if (notification_state_->exchange(NotificationState::kWorkingNotified) == NotificationState::kSleeping) { |
153 | | - NotifyAsyncConsumer(); |
154 | | - } |
155 | | -} |
156 | | - |
157 | | -template <auto TryStartWaiting> |
158 | | -void AsyncFlatCombiningQueue::Wait() noexcept { |
159 | | - UASSERT(!has_waiter_.exchange(true)); |
160 | | - auto& current = engine::current_task::GetCurrentTaskContext(); |
161 | | - // Check before writing to avoid excessive CPU cache invalidation. |
162 | | - if (consuming_task_context_ != ¤t) { |
163 | | - consuming_task_context_ = ¤t; |
164 | | - } |
165 | | - // No deadlines or cancellations are allowed, otherwise this task may walk away and be destroyed, |
166 | | - // and the notification will be sent to a dead task. |
167 | | - UASSERT(!current.IsCancellable()); |
168 | | - |
169 | | - Awaitable<TryStartWaiting> awaitable{*this}; |
170 | | - [[maybe_unused]] const auto wakeup_source = current.Sleep(awaitable, Deadline{}); |
171 | | - UASSERT(wakeup_source == TaskContext::WakeupSource::kNotify); |
172 | | - |
173 | | - UASSERT(consuming_task_context_ == ¤t); |
174 | | - UASSERT(has_waiter_.exchange(false)); |
175 | | -} |
176 | | - |
177 | | -bool AsyncFlatCombiningQueue::TryStartWaitingWhileEmpty() { |
178 | | - // Returns whether we are going to sleep. If a notification arrived while we |
179 | | - // were working (state is kWorkingNotified), the CAS fails and we keep |
180 | | - // working, re-checking the queue without sleeping. |
181 | | - auto expected = NotificationState::kWorking; |
182 | | - return notification_state_->compare_exchange_strong(expected, NotificationState::kSleeping); |
| 111 | + // If the consumer is sleeping, this wakes it up. If it is (already) |
| 112 | + // working, it will re-check the queue before sleeping again, so no wakeup |
| 113 | + // is needed; Send() only sets the signal flag in that case. |
| 114 | + empty_queue_event_->Send(); |
183 | 115 | } |
184 | 116 |
|
185 | 117 | bool AsyncFlatCombiningQueue::TryStartWaitingForConsumer() { |
|
0 commit comments