Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit d74bebe

Browse files
committed
Update async_generator<T> to use tail-call resumption.
This greatly simplifies the implementation as we don't need to do anything special for the case where the async operations complete synchronously. The async_generator<T> no longer needs atomic operations to synchronise consumer/produce running concurrently since one is always suspended before resuming the other.
1 parent d991920 commit d74bebe

File tree

2 files changed

+90
-313
lines changed

2 files changed

+90
-313
lines changed

include/cppcoro/async_generator.hpp

Lines changed: 18 additions & 249 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,10 @@
88
#include <cppcoro/fmap.hpp>
99

1010
#include <exception>
11-
#include <atomic>
1211
#include <iterator>
1312
#include <type_traits>
1413
#include <experimental/coroutine>
1514
#include <functional>
16-
#include <cassert>
1715

1816
namespace cppcoro
1917
{
@@ -32,8 +30,7 @@ namespace cppcoro
3230
public:
3331

3432
async_generator_promise_base() noexcept
35-
: m_state(state::value_ready_producer_suspended)
36-
, m_exception(nullptr)
33+
: m_exception(nullptr)
3734
{
3835
// Other variables left intentionally uninitialised as they're
3936
// only referenced in certain states by which time they should
@@ -52,12 +49,7 @@ namespace cppcoro
5249

5350
void unhandled_exception() noexcept
5451
{
55-
// Don't bother capturing the exception if we have been cancelled
56-
// as there is no consumer that will see it.
57-
if (m_state.load(std::memory_order_relaxed) != state::cancelled)
58-
{
59-
m_exception = std::current_exception();
60-
}
52+
m_exception = std::current_exception();
6153
}
6254

6355
void return_void() noexcept
@@ -81,30 +73,6 @@ namespace cppcoro
8173
}
8274
}
8375

84-
/// Request that the generator cancel generation of new items.
85-
///
86-
/// \return
87-
/// Returns true if the request was completed synchronously and the associated
88-
/// producer coroutine is now available to be destroyed. In which case the caller
89-
/// is expected to call destroy() on the coroutine_handle.
90-
/// Returns false if the producer coroutine was not at a suitable suspend-point.
91-
/// The coroutine will be destroyed when it next reaches a co_yield or co_return
92-
/// statement.
93-
bool request_cancellation() noexcept
94-
{
95-
const auto previousState = m_state.exchange(state::cancelled, std::memory_order_acq_rel);
96-
97-
// Not valid to destroy async_generator<T> object if consumer coroutine still suspended
98-
// in a co_await for next item.
99-
assert(previousState != state::value_not_ready_consumer_suspended);
100-
101-
// A coroutine should only ever be cancelled once, from the destructor of the
102-
// owning async_generator<T> object.
103-
assert(previousState != state::cancelled);
104-
105-
return previousState == state::value_ready_producer_suspended;
106-
}
107-
10876
protected:
10977

11078
async_generator_yield_operation internal_yield_value() noexcept;
@@ -114,38 +82,6 @@ namespace cppcoro
11482
friend class async_generator_yield_operation;
11583
friend class async_generator_advance_operation;
11684

117-
// State transition diagram
118-
// VNRCA - value_not_ready_consumer_active
119-
// VNRCS - value_not_ready_consumer_suspended
120-
// VRPA - value_ready_consumer_active
121-
// VRPS - value_ready_consumer_suspended
122-
//
123-
// A +--- VNRCA --[C]--> VNRCS yield_value()
124-
// | | | A | A | .
125-
// | [C] [P] | [P] | | .
126-
// | | | [C] | [C] | .
127-
// | | V | V | | .
128-
// operator++/ | VRPS <--[P]--- VRPA V |
129-
// begin() | | | |
130-
// | [C] [C] |
131-
// | +----+ +---+ |
132-
// | | | |
133-
// | V V V
134-
// +--------> cancelled ~async_generator()
135-
//
136-
// [C] - Consumer performs this transition
137-
// [P] - Producer performs this transition
138-
enum class state
139-
{
140-
value_not_ready_consumer_active,
141-
value_not_ready_consumer_suspended,
142-
value_ready_producer_active,
143-
value_ready_producer_suspended,
144-
cancelled
145-
};
146-
147-
std::atomic<state> m_state;
148-
14985
std::exception_ptr m_exception;
15086

15187
std::experimental::coroutine_handle<> m_consumerCoroutine;
@@ -157,27 +93,28 @@ namespace cppcoro
15793

15894
class async_generator_yield_operation final
15995
{
160-
using state = async_generator_promise_base::state;
161-
16296
public:
16397

164-
async_generator_yield_operation(async_generator_promise_base& promise, state initialState) noexcept
165-
: m_promise(promise)
166-
, m_initialState(initialState)
98+
async_generator_yield_operation(std::experimental::coroutine_handle<> consumer) noexcept
99+
: m_consumer(consumer)
167100
{}
168101

169102
bool await_ready() const noexcept
170103
{
171-
return m_initialState == state::value_not_ready_consumer_suspended;
104+
return false;
172105
}
173106

174-
bool await_suspend(std::experimental::coroutine_handle<> producer) noexcept;
107+
std::experimental::coroutine_handle<>
108+
await_suspend(std::experimental::coroutine_handle<> producer) noexcept
109+
{
110+
return m_consumer;
111+
}
175112

176113
void await_resume() noexcept {}
177114

178115
private:
179-
async_generator_promise_base& m_promise;
180-
state m_initialState;
116+
117+
std::experimental::coroutine_handle<> m_consumer;
181118

182119
};
183120

@@ -189,112 +126,11 @@ namespace cppcoro
189126

190127
inline async_generator_yield_operation async_generator_promise_base::internal_yield_value() noexcept
191128
{
192-
state currentState = m_state.load(std::memory_order_acquire);
193-
assert(currentState != state::value_ready_producer_active);
194-
assert(currentState != state::value_ready_producer_suspended);
195-
196-
if (currentState == state::value_not_ready_consumer_suspended)
197-
{
198-
// Only need relaxed memory order since we're resuming the
199-
// consumer on the same thread.
200-
m_state.store(state::value_ready_producer_active, std::memory_order_relaxed);
201-
202-
// Resume the consumer.
203-
// It might ask for another value before returning, in which case it'll
204-
// transition to value_not_ready_consumer_suspended and we can return from
205-
// yield_value without suspending, otherwise we should try to suspend
206-
// the producer in which case the consumer will wake us up again
207-
// when it wants the next value.
208-
m_consumerCoroutine.resume();
209-
210-
// Need to use acquire semantics here since it's possible that the
211-
// consumer might have asked for the next value on a different thread
212-
// which executed concurrently with the call to m_consumerCoro on the
213-
// current thread above.
214-
currentState = m_state.load(std::memory_order_acquire);
215-
}
216-
217-
return async_generator_yield_operation{ *this, currentState };
218-
}
219-
220-
inline bool async_generator_yield_operation::await_suspend(
221-
std::experimental::coroutine_handle<> producer) noexcept
222-
{
223-
state currentState = m_initialState;
224-
if (currentState == state::value_not_ready_consumer_active)
225-
{
226-
bool producerSuspended = m_promise.m_state.compare_exchange_strong(
227-
currentState,
228-
state::value_ready_producer_suspended,
229-
std::memory_order_release,
230-
std::memory_order_acquire);
231-
if (producerSuspended)
232-
{
233-
return true;
234-
}
235-
236-
if (currentState == state::value_not_ready_consumer_suspended)
237-
{
238-
// Can get away with using relaxed memory semantics here since we're
239-
// resuming the consumer on the current thread.
240-
m_promise.m_state.store(state::value_ready_producer_active, std::memory_order_relaxed);
241-
242-
m_promise.m_consumerCoroutine.resume();
243-
244-
// The consumer might have asked for another value before returning, in which case
245-
// it'll transition to value_not_ready_consumer_suspended and we can return without
246-
// suspending, otherwise we should try to suspend the producer, in which case the
247-
// consumer will wake us up again when it wants the next value.
248-
//
249-
// Need to use acquire semantics here since it's possible that the consumer might
250-
// have asked for the next value on a different thread which executed concurrently
251-
// with the call to m_consumerCoro.resume() above.
252-
currentState = m_promise.m_state.load(std::memory_order_acquire);
253-
if (currentState == state::value_not_ready_consumer_suspended)
254-
{
255-
return false;
256-
}
257-
}
258-
}
259-
260-
// By this point the consumer has been resumed if required and is now active.
261-
262-
if (currentState == state::value_ready_producer_active)
263-
{
264-
// Try to suspend the producer.
265-
// If we failed to suspend then it's either because the consumer destructed, transitioning
266-
// the state to cancelled, or requested the next item, transitioning the state to value_not_ready_consumer_suspended.
267-
const bool suspendedProducer = m_promise.m_state.compare_exchange_strong(
268-
currentState,
269-
state::value_ready_producer_suspended,
270-
std::memory_order_release,
271-
std::memory_order_acquire);
272-
if (suspendedProducer)
273-
{
274-
return true;
275-
}
276-
277-
if (currentState == state::value_not_ready_consumer_suspended)
278-
{
279-
// Consumer has asked for the next value.
280-
return false;
281-
}
282-
}
283-
284-
assert(currentState == state::cancelled);
285-
286-
// async_generator object has been destroyed and we're now at a
287-
// co_yield/co_return suspension point so we can just destroy
288-
// the coroutine.
289-
producer.destroy();
290-
291-
return true;
129+
return async_generator_yield_operation{ m_consumerCoroutine };
292130
}
293131

294132
class async_generator_advance_operation
295133
{
296-
using state = async_generator_promise_base::state;
297-
298134
protected:
299135

300136
async_generator_advance_operation(std::nullptr_t) noexcept
@@ -308,88 +144,24 @@ namespace cppcoro
308144
: m_promise(std::addressof(promise))
309145
, m_producerCoroutine(producerCoroutine)
310146
{
311-
state initialState = promise.m_state.load(std::memory_order_acquire);
312-
if (initialState == state::value_ready_producer_suspended)
313-
{
314-
// Can use relaxed memory order here as we will be resuming the producer
315-
// on the same thread.
316-
promise.m_state.store(state::value_not_ready_consumer_active, std::memory_order_relaxed);
317-
318-
producerCoroutine.resume();
319-
320-
// Need to use acquire memory order here since it's possible that the
321-
// coroutine may have transferred execution to another thread and
322-
// completed on that other thread before the call to resume() returns.
323-
initialState = promise.m_state.load(std::memory_order_acquire);
324-
}
325-
326-
m_initialState = initialState;
327147
}
328148

329149
public:
330150

331-
bool await_ready() const noexcept
332-
{
333-
return m_initialState == state::value_ready_producer_suspended;
334-
}
151+
bool await_ready() const noexcept { return false; }
335152

336-
bool await_suspend(std::experimental::coroutine_handle<> consumerCoroutine) noexcept
153+
std::experimental::coroutine_handle<>
154+
await_suspend(std::experimental::coroutine_handle<> consumerCoroutine) noexcept
337155
{
338156
m_promise->m_consumerCoroutine = consumerCoroutine;
339-
340-
auto currentState = m_initialState;
341-
if (currentState == state::value_ready_producer_active)
342-
{
343-
// A potential race between whether consumer or producer coroutine
344-
// suspends first. Resolve the race using a compare-exchange.
345-
if (m_promise->m_state.compare_exchange_strong(
346-
currentState,
347-
state::value_not_ready_consumer_suspended,
348-
std::memory_order_release,
349-
std::memory_order_acquire))
350-
{
351-
return true;
352-
}
353-
354-
assert(currentState == state::value_ready_producer_suspended);
355-
356-
m_promise->m_state.store(state::value_not_ready_consumer_active, std::memory_order_relaxed);
357-
358-
m_producerCoroutine.resume();
359-
360-
currentState = m_promise->m_state.load(std::memory_order_acquire);
361-
if (currentState == state::value_ready_producer_suspended)
362-
{
363-
// Producer coroutine produced a value synchronously.
364-
return false;
365-
}
366-
}
367-
368-
assert(currentState == state::value_not_ready_consumer_active);
369-
370-
// Try to suspend consumer coroutine, transitioning to value_not_ready_consumer_suspended.
371-
// This could be racing with producer making the next value available and suspending
372-
// (transition to value_ready_producer_suspended) so we use compare_exchange to decide who
373-
// wins the race.
374-
// If compare_exchange succeeds then consumer suspended (and we return true).
375-
// If it fails then producer yielded next value and suspended and we can return
376-
// synchronously without suspended (ie. return false).
377-
return m_promise->m_state.compare_exchange_strong(
378-
currentState,
379-
state::value_not_ready_consumer_suspended,
380-
std::memory_order_release,
381-
std::memory_order_acquire);
157+
return m_producerCoroutine;
382158
}
383159

384160
protected:
385161

386162
async_generator_promise_base* m_promise;
387163
std::experimental::coroutine_handle<> m_producerCoroutine;
388164

389-
private:
390-
391-
state m_initialState;
392-
393165
};
394166

395167
template<typename T>
@@ -594,10 +366,7 @@ namespace cppcoro
594366
{
595367
if (m_coroutine)
596368
{
597-
if (m_coroutine.promise().request_cancellation())
598-
{
599-
m_coroutine.destroy();
600-
}
369+
m_coroutine.destroy();
601370
}
602371
}
603372

0 commit comments

Comments
 (0)