Skip to content

Commit 236fc8f

Browse files
authored
♻️ extend async context blocking mechanisms and add comprehensive tests (#26)
This PR significantly expands the async context API to support multiple blocking states and adds extensive test coverage. Key changes include: - Add new blocking methods: block_by_sync() with context pointer, and block_by_external() to support different synchronization patterns - Make blocking methods return std::suspend_always for improved coroutine suspension ergonomics - Add context_token class for safe context capture and unblocking operations - Enhance context state queries with safer std::holds_alternative checks - Add git-town configuration for workflow management - Add comprehensive test suite demonstrating resource synchronization patterns between coroutines, including mutex-like behavior and I/O blocking scenarios - Refactor test infrastructure with reusable test_scheduler implementation. These enhancements enable more sophisticated coroutine coordination patterns while maintaining the lightweight design of the async context system.
1 parent 8b09c85 commit 236fc8f

File tree

5 files changed

+389
-78
lines changed

5 files changed

+389
-78
lines changed

git-town.toml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
# See https://www.git-town.com/configuration-file for details
2+
3+
[branches]
4+
main = "main"
5+
6+
[hosting]
7+
forge-type = "github"
8+
github-connector-type = "gh"

modules/async_context.cppm

Lines changed: 127 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,8 @@ using sleep_duration = std::chrono::nanoseconds;
118118
export class scheduler
119119
{
120120
public:
121+
using block_info = std::variant<std::monostate, sleep_duration, context*>;
122+
121123
/**
122124
* @brief
123125
*
@@ -142,7 +144,7 @@ public:
142144
*/
143145
void schedule(context& p_context,
144146
blocked_by p_block_state,
145-
std::variant<sleep_duration, context*> p_block_info)
147+
block_info p_block_info) noexcept
146148
{
147149
return do_schedule(p_context, p_block_state, p_block_info);
148150
}
@@ -164,10 +166,9 @@ public:
164166
}
165167

166168
private:
167-
virtual void do_schedule(
168-
context& p_context,
169-
blocked_by p_block_state,
170-
std::variant<sleep_duration, context*> p_block_info) = 0;
169+
virtual void do_schedule(context& p_context,
170+
blocked_by p_block_state,
171+
block_info p_block_info) noexcept = 0;
171172

172173
virtual std::pmr::memory_resource& do_get_allocator() noexcept = 0;
173174
};
@@ -196,25 +197,38 @@ public:
196197
m_stack = { allocator.allocate_object<byte>(p_stack_size), p_stack_size };
197198
}
198199

199-
void unblock()
200+
void unblock() noexcept
200201
{
201-
transition_to(blocked_by::nothing, default_timeout);
202+
transition_to(blocked_by::nothing);
202203
}
204+
203205
void unblock_without_notification()
204206
{
205207
m_state = blocked_by::nothing;
206208
}
207-
void block_by_time(sleep_duration p_duration)
209+
210+
std::suspend_always block_by_time(sleep_duration p_duration)
208211
{
209212
transition_to(blocked_by::time, p_duration);
213+
return {};
210214
}
211-
void block_by_io(sleep_duration p_duration = default_timeout)
215+
216+
std::suspend_always block_by_io(sleep_duration p_duration = default_timeout)
212217
{
213218
transition_to(blocked_by::io, p_duration);
219+
return {};
214220
}
215-
void block_by_sync(sleep_duration p_duration = default_timeout)
221+
222+
std::suspend_always block_by_sync(context* p_blocker)
216223
{
217-
transition_to(blocked_by::sync, p_duration);
224+
transition_to(blocked_by::sync, p_blocker);
225+
return {};
226+
}
227+
228+
std::suspend_always block_by_external()
229+
{
230+
transition_to(blocked_by::external, std::monostate{});
231+
return {};
218232
}
219233

220234
[[nodiscard]] constexpr std::coroutine_handle<> active_handle() const
@@ -224,7 +238,10 @@ public:
224238

225239
[[nodiscard]] auto state() const
226240
{
227-
return std::get<1>(m_state);
241+
if (std::holds_alternative<blocked_by>(m_state)) {
242+
return std::get<blocked_by>(m_state);
243+
}
244+
return blocked_by::nothing;
228245
}
229246

230247
constexpr void active_handle(std::coroutine_handle<> p_active_handle)
@@ -265,10 +282,28 @@ public:
265282

266283
constexpr auto last_allocation_size()
267284
{
268-
return std::get<usize>(m_state);
285+
if (std::holds_alternative<usize>(m_state)) {
286+
return std::get<usize>(m_state);
287+
}
288+
return 0uz;
269289
}
270290

271-
void transition_to(blocked_by p_new_state, sleep_duration p_info)
291+
~context()
292+
{
293+
using poly_allocator = std::pmr::polymorphic_allocator<byte>;
294+
auto allocator = poly_allocator(&m_scheduler->get_allocator());
295+
allocator.deallocate_object<byte>(m_stack.data(), m_stack.size());
296+
};
297+
298+
private:
299+
friend class promise_base;
300+
template<typename T>
301+
friend class future_promise_type;
302+
303+
using context_state = std::variant<usize, blocked_by, std::exception_ptr>;
304+
305+
void transition_to(blocked_by p_new_state,
306+
scheduler::block_info p_info = std::monostate{}) noexcept
272307
{
273308
m_state = p_new_state;
274309
m_scheduler->schedule(*this, p_new_state, p_info);
@@ -296,17 +331,6 @@ public:
296331
m_state = p_exception;
297332
}
298333

299-
~context()
300-
{
301-
using poly_allocator = std::pmr::polymorphic_allocator<byte>;
302-
auto allocator = poly_allocator(&m_scheduler->get_allocator());
303-
allocator.deallocate_object<byte>(m_stack.data(), m_stack.size());
304-
};
305-
306-
private:
307-
friend class promise_base;
308-
using context_state = std::variant<usize, blocked_by, std::exception_ptr>;
309-
310334
// Should stay within a standard cache-line of 64 bytes (8 words)
311335
mem::strong_ptr<scheduler> m_scheduler; // word 1-2
312336
std::coroutine_handle<> m_active_handle = std::noop_coroutine(); // word 3
@@ -315,8 +339,82 @@ private:
315339
context_state m_state{ 0uz }; // word 7-8
316340
};
317341

318-
static_assert(sizeof(context) <=
319-
std::hardware_constructive_interference_size * 2);
342+
export class context_token
343+
{
344+
public:
345+
constexpr context_token() = default;
346+
constexpr context_token(context& p_capture) noexcept
347+
: m_context_address(std::bit_cast<std::uintptr_t>(&p_capture))
348+
{
349+
}
350+
constexpr context_token& operator=(context& p_capture) noexcept
351+
{
352+
m_context_address = std::bit_cast<std::uintptr_t>(&p_capture);
353+
return *this;
354+
}
355+
constexpr context_token& operator=(nullptr_t) noexcept
356+
{
357+
m_context_address = 0U;
358+
return *this;
359+
}
360+
361+
constexpr context_token(context_token const& p_capture) noexcept = default;
362+
constexpr context_token& operator=(context_token const& p_capture) noexcept =
363+
default;
364+
constexpr context_token(context_token&& p_capture) noexcept = default;
365+
constexpr context_token& operator=(context_token& p_capture) noexcept =
366+
default;
367+
368+
constexpr bool operator==(context& p_context) noexcept
369+
{
370+
return m_context_address == std::bit_cast<std::uintptr_t>(&p_context);
371+
}
372+
373+
[[nodiscard]] constexpr bool in_use() const noexcept
374+
{
375+
return m_context_address != 0U;
376+
}
377+
378+
[[nodiscard]] auto address() const noexcept
379+
{
380+
return m_context_address != 0U;
381+
}
382+
383+
[[nodiscard]] constexpr operator bool() const noexcept
384+
{
385+
return in_use();
386+
}
387+
388+
constexpr void lease(context& p_capture) noexcept
389+
{
390+
m_context_address = std::bit_cast<std::uintptr_t>(&p_capture);
391+
}
392+
393+
constexpr std::suspend_always set_as_block_by_sync(context& p_capture)
394+
{
395+
if (in_use()) {
396+
auto* address = std::bit_cast<void*>(m_context_address);
397+
auto* inner_context = static_cast<context*>(address);
398+
p_capture.block_by_sync(inner_context);
399+
}
400+
return {};
401+
}
402+
403+
constexpr void unblock_and_clear() noexcept
404+
{
405+
if (in_use()) {
406+
auto* address = std::bit_cast<void*>(m_context_address);
407+
auto* inner_context = static_cast<context*>(address);
408+
inner_context->unblock();
409+
m_context_address = 0U;
410+
}
411+
}
412+
413+
private:
414+
std::uintptr_t m_context_address = 0U;
415+
};
416+
417+
static_assert(sizeof(context) <= std::hardware_constructive_interference_size);
320418

321419
// =============================================================================
322420
//
@@ -398,8 +496,7 @@ public:
398496
constexpr auto await_transform(
399497
std::chrono::duration<Rep, Ratio> p_sleep_duration) noexcept
400498
{
401-
m_context->block_by_time(p_sleep_duration);
402-
return std::suspend_always{};
499+
return m_context->block_by_time(p_sleep_duration);
403500
}
404501

405502
constexpr auto await_transform(pop_active_coroutine) noexcept
@@ -624,8 +721,7 @@ public:
624721

625722
constexpr void resume() const
626723
{
627-
auto active = handle().promise().get_context().active_handle();
628-
active.resume();
724+
handle().promise().get_context().active_handle().resume();
629725
}
630726

631727
/**

test_package/main.cpp

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -22,22 +22,21 @@
2222

2323
import async_context;
2424

25-
struct my_scheduler
25+
struct test_scheduler
2626
: public async::scheduler
27-
, mem::enable_strong_from_this<my_scheduler>
27+
, mem::enable_strong_from_this<test_scheduler>
2828
{
2929
int sleep_count = 0;
3030

31-
my_scheduler(mem::strong_ptr_only_token)
31+
test_scheduler(mem::strong_ptr_only_token)
3232
{
3333
}
3434

3535
private:
36-
void do_schedule(
37-
[[maybe_unused]] async::context& p_context,
38-
[[maybe_unused]] async::blocked_by p_block_state,
39-
[[maybe_unused]] std::variant<std::chrono::nanoseconds, async::context*>
40-
p_block_info) override
36+
void do_schedule([[maybe_unused]] async::context& p_context,
37+
[[maybe_unused]] async::blocked_by p_block_state,
38+
[[maybe_unused]] async::scheduler::block_info
39+
p_block_info) noexcept override
4140
{
4241
if (std::holds_alternative<std::chrono::nanoseconds>(p_block_info)) {
4342
sleep_count++;
@@ -64,7 +63,7 @@ async::future<void> coro_double_delay(async::context&)
6463
int main()
6564
{
6665
auto scheduler =
67-
mem::make_strong_ptr<my_scheduler>(std::pmr::new_delete_resource());
66+
mem::make_strong_ptr<test_scheduler>(std::pmr::new_delete_resource());
6867
async::context my_context(scheduler, 1024);
6968

7069
auto future_delay = coro_double_delay(my_context);

0 commit comments

Comments
 (0)