Skip to content

Commit 3939e6a

Browse files
[V4] Null checkpoints (#75)
* definition of the null state * idea comment * starting the null stack * move initialization inside constructor * move outside of frame * don't set default * static assert to catch bad catagory * todo * spell * generalise lock/key * use opaque checkpoint * acq/rel * move init to constructor * get context/alloc helpers * assume empty on acquire * docs * spell * fix empty bug + expose * strong exception safety * add mission import * spell
1 parent dae2081 commit 3939e6a

10 files changed

Lines changed: 215 additions & 88 deletions

File tree

benchmark/src/libfork_benchmark/fib/lf_parts.cpp

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,6 @@ struct deque_ctx {
8686
void post(lf::await_handle<deque_ctx>) {}
8787

8888
// TODO: try LF_NO_INLINE for final allocator
89-
LF_NO_INLINE
9089
void push(handle_type handle) { work.push(handle); }
9190

9291
auto pop() noexcept -> handle_type {

src/core/concepts.cxx

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -62,19 +62,23 @@ consteval auto constify(T &&x) noexcept -> std::add_const_t<T> &;
6262
*
6363
* // TODO: define if release is required before acquire?
6464
*
65-
* - After construction `this` is in the empty state and push is valid.
65+
* - After construction `this` is empty and push is valid.
6666
* - Pop is valid provided the FILO order is respected.
6767
* - Push produces pointers aligned to __STDCPP_DEFAULT_NEW_ALIGNMENT__.
6868
* - Destruction is expected to only occur when the stack is empty.
6969
* - Result of `.checkpoint()` is expected to:
7070
* - Be "cheap to copy".
71-
* - Compare equal if and only if they belong to the same stack.
71+
* - Have a null state (default constructed) that only compares equal to itself.
72+
* - Is allowed to return null if push has never been called.
73+
* - Compare equal if and only if they belong to the same stack or are both null.
7274
* - Have no preconditions about when it's called.
73-
* - A default constructed checkpoint considered undefined
74-
* - Release detaches the current stack and leaves `this` in the empty state.
75+
* - Prepare release puts the stack into a state which another thread can acquire it.
76+
* - Release detaches the current stack and leaves `this` empty.
77+
* - This may be called concurrently with acquire
7578
* - Acquire attaches to the stack that the checkpoint came from:
76-
* - This is a noop if the checkpoint is from the current stack.
77-
* - Otherwise `this` is empty.
79+
* - It is only called the stack is empty.
80+
* - It is only called with a checkpoint not equal to the current checkpoint.
81+
* - It is called after prepare release (and no other functions in between)
7882
*
7983
* Fast-path operations: empty, push, pop, checkpoint
8084
* Slow-path operations: release, acquire

src/core/frame.cxx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import std;
77

88
import :concepts;
99
import :constants;
10+
import :utility;
11+
import :thread_locals;
1012

1113
namespace lf {
1214
// =================== Cancellation =================== //
@@ -72,13 +74,16 @@ struct frame_type {
7274

7375
// == Member variables == //
7476

77+
// TODO: add checked accessors for all the things (including except etc)
78+
7579
union {
7680
parent_union parent;
7781
except_type *except;
7882
};
7983

8084
cancellation *cancel;
8185

86+
// TODO: drop default constructible requirement?
8287
[[no_unique_address]]
8388
checkpoint_type stack_ckpt;
8489

@@ -92,7 +97,7 @@ struct frame_type {
9297
// Explicitly post construction, this allows the compiler to emit a single
9398
// instruction for the zero init then an instruction for the joins init,
9499
// instead of three instructions.
95-
constexpr frame_type() noexcept { joins = k_u16_max; }
100+
constexpr frame_type(checkpoint_type &&ckpt) noexcept : stack_ckpt(std::move(ckpt)) { joins = k_u16_max; }
96101

97102
[[nodiscard]]
98103
constexpr auto is_cancelled() const noexcept -> bool {

src/core/handles.cxx

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,10 @@ export module libfork.core:handles;
33
import std;
44

55
import :frame;
6+
import :utility;
67

78
namespace lf {
89

9-
// =================== Lock and key =================== //
10-
11-
struct lock {};
12-
13-
inline constexpr lock key = {};
14-
1510
// =================== Frame =================== //
1611

1712
// TODO: api + test this is lock-free
@@ -28,7 +23,7 @@ export template <typename T>
2823
class frame_handle {
2924
public:
3025
constexpr frame_handle() = default;
31-
constexpr frame_handle(lock, frame_type<T> *ptr) noexcept : m_ptr{ptr} {}
26+
constexpr frame_handle(key_t, frame_type<T> *ptr) noexcept : m_ptr{ptr} {}
3227

3328
constexpr auto operator==(frame_handle const &) const noexcept -> bool = default;
3429

@@ -48,7 +43,7 @@ export template <typename T>
4843
class await_handle {
4944
public:
5045
constexpr await_handle() = default;
51-
constexpr await_handle(lock, frame_type<T> *ptr) noexcept : m_ptr{ptr} {}
46+
constexpr await_handle(key_t, frame_type<T> *ptr) noexcept : m_ptr{ptr} {}
5247

5348
constexpr auto operator==(await_handle const &) const noexcept -> bool = default;
5449

src/core/promise.cxx

Lines changed: 30 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -119,15 +119,20 @@ final_suspend_continue(Context *context, frame_type<Context> *parent) noexcept -
119119
// the stack so we must prepare it for release now.
120120
auto release_key = context->allocator().prepare_release();
121121

122+
// TODO: we could add an `if (owner)` around acquire below, then we could
123+
// define that acquire is always called with null or not-self.
124+
122125
// Register with parent we have completed this child task.
123126
if (parent->atomic_joins().fetch_sub(1, std::memory_order_release) == 1) {
124127
// Parent has reached join and we are the last child task to complete. We
125128
// are the exclusive owner of the parent and therefore, we must continue
126129
// parent. As we won the race, acquire all writes before resuming.
127130
std::atomic_thread_fence(std::memory_order_acquire);
128131

129-
// In case of scenario (2) we must acquire the parent's stack.
130-
context->allocator().acquire(std::as_const(parent->stack_ckpt));
132+
if (!owner) {
133+
// In case of scenario (2) we must acquire the parent's stack.
134+
context->allocator().acquire(std::as_const(parent->stack_ckpt));
135+
}
131136

132137
// Must reset parent's control block before resuming parent.
133138
parent->reset_counters();
@@ -192,11 +197,11 @@ constexpr auto final_suspend(frame_type<Context> *frame) noexcept -> coro<> {
192197
return parent->handle();
193198
case category::fork:
194199

195-
Context *context = not_null(thread_context<Context>);
200+
Context *context = get_context<Context>();
196201

197202
if (frame_handle last_pushed = context->pop()) {
198203
// No-one stole continuation, we are the exclusive owner of parent -> just keep ripping!
199-
LF_ASSUME(last_pushed == frame_handle{key, parent});
204+
LF_ASSUME(last_pushed == frame_handle{key(), parent});
200205
// This is not a join point so no state (i.e. counters) is guaranteed.
201206
return parent->handle();
202207
}
@@ -243,6 +248,8 @@ constexpr void stash_current_exception(frame_type<Context> *frame) noexcept {
243248
template <category Cat, worker_context Context>
244249
struct awaitable : std::suspend_always {
245250

251+
static_assert(Cat == category::call || Cat == category::fork, "Invalid category for awaitable");
252+
246253
frame_type<Context> *child;
247254

248255
/**
@@ -274,16 +281,21 @@ struct awaitable : std::suspend_always {
274281
// Propagate parent->child relationships
275282
self.child->parent.frame = &parent.promise().frame;
276283
self.child->cancel = parent.promise().frame.cancel;
277-
self.child->stack_ckpt = not_null(thread_context<Context>)->allocator().checkpoint();
278-
self.child->kind = Cat;
284+
285+
if constexpr (Cat == category::call) {
286+
// Should be the default
287+
LF_ASSUME(self.child->kind == category::call);
288+
} else {
289+
self.child->kind = Cat;
290+
}
279291

280292
if constexpr (Cat == category::fork) {
281293
// It is critical to pass self by-value here, after the call to push()
282294
// the object `*this` may be destroyed, if passing by ref it would be
283295
// use-after-free to then access self in the following line to fetch the
284296
// handle.
285297
LF_TRY {
286-
not_null(thread_context<Context>)->push(frame_handle{key, &parent.promise().frame});
298+
get_context<Context>()->push(frame_handle{key(), &parent.promise().frame});
287299
} LF_CATCH_ALL {
288300
return self.stash_and_resume(parent), parent;
289301
}
@@ -303,7 +315,7 @@ struct join_awaitable {
303315
frame_type<Context> *frame;
304316

305317
constexpr auto take_stack_and_reset(this join_awaitable self) noexcept -> void {
306-
Context *context = not_null(thread_context<Context>);
318+
Context *context = get_context<Context>();
307319
LF_ASSUME(self.frame->stack_ckpt != context->allocator().checkpoint());
308320
context->allocator().acquire(std::as_const(self.frame->stack_ckpt));
309321
self.frame->reset_counters();
@@ -437,14 +449,12 @@ struct mixin_frame {
437449
// --- Allocation
438450

439451
static auto operator new(std::size_t sz) -> void * {
440-
void *ptr = not_null(thread_context<Ctx>)->allocator().push(sz);
452+
void *ptr = get_allocator<Ctx>().push(sz);
441453
LF_ASSUME(is_aligned<k_new_align>(ptr));
442454
return std::assume_aligned<k_new_align>(ptr);
443455
}
444456

445-
static auto operator delete(void *p, std::size_t sz) noexcept -> void {
446-
not_null(thread_context<Ctx>)->allocator().pop(p, sz);
447-
}
457+
static auto operator delete(void *p, std::size_t sz) noexcept -> void { get_allocator<Ctx>().pop(p, sz); }
448458

449459
// --- Await transformations
450460

@@ -506,7 +516,10 @@ struct mixin_frame {
506516
template <worker_context Context>
507517
struct promise_type<void, Context> : mixin_frame<Context> {
508518

509-
frame_type<Context> frame;
519+
// Putting init here allows:
520+
// 1. Frame not no need to know about the checkpoint type
521+
// 2. Compiler merge double read of thread local here and in allocator
522+
frame_type<Context> frame{get_allocator<Context>().checkpoint()};
510523

511524
constexpr auto get_return_object() noexcept -> task<void> { return access::task(this); }
512525

@@ -518,7 +531,10 @@ struct promise_type<void, Context> : mixin_frame<Context> {
518531
template <returnable T, worker_context Context>
519532
struct promise_type : mixin_frame<Context> {
520533

521-
frame_type<Context> frame;
534+
// Putting init here allows:
535+
// 1. Frame not no need to know about the checkpoint type
536+
// 2. Compiler merge double read of thread local here and in allocator
537+
frame_type<Context> frame{get_allocator<Context>().checkpoint()};
522538
T *return_address;
523539

524540
constexpr auto get_return_object() noexcept -> task<T> { return access::task(this); }

src/core/schedule.cxx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@ constexpr auto schedule(Context *context, Fn &&fn, Args &&...args) noexcept -> a
4545

4646
// TODO: make sure this is exception safe and correctly qualifed
4747

48+
LF_ASSUME(context != nullptr);
49+
4850
// This is what the async function will return.
4951
using result_type = async_result_t<Fn, Context, Args...>;
5052

@@ -54,15 +56,15 @@ constexpr auto schedule(Context *context, Fn &&fn, Args &&...args) noexcept -> a
5456
// TODO: make sure we're cancel safe
5557

5658
// TODO: Before doing this we must be on a valid context.
57-
LF_ASSUME(thread_context<Context> == context);
59+
LF_ASSUME(get_context<Context>() == context);
5860

5961
auto *promise = access::promise<Context>(
6062
std::invoke(std::forward<Fn>(fn), env<Context>{}, std::forward<Args>(args)...));
6163

6264
// TODO: expose cancellable?
6365
promise->frame.parent.block = root_block.get();
6466
promise->frame.cancel = nullptr;
65-
promise->frame.stack_ckpt = thread_context<Context>->allocator().checkpoint();
67+
promise->frame.stack_ckpt = get_allocator<Context>().checkpoint();
6668
promise->frame.kind = lf::category::root;
6769

6870
if constexpr (!std::is_void_v<result_type>) {

0 commit comments

Comments
 (0)