Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 5 additions & 7 deletions doc/design/lifetime-contract.md
Original file line number Diff line number Diff line change
Expand Up @@ -181,17 +181,15 @@ reach an unrelated file:
## 6. Teardown

`io_context` destruction runs every service's `shutdown()` newest
first, then deletes the services in the same order. Services created
after the scheduler come before it; the timer service, which the
scheduler's constructor creates, comes after it. Every service gives
up all its work in `shutdown()` and posts nothing from its destructor.
first, then deletes the services in the same order. The scheduler is
created with the context, and every other service after it, so every
service shuts down before the scheduler drains. Every service gives up
all its work in `shutdown()` and posts nothing from its destructor.
So nothing reaches the queue after the drain in step 3. Waiting for
the kernel or the OS to return operations a service submitted, as in
step 2, is part of giving up its work. The contract for that
sequence:

0. **Timers first.** Every scheduler shuts the timer service down
before it drains, so work a timer's teardown posts is drained too.
1. **Cancel.** Each service cancels or closes every live
implementation. Its pool's `shutdown()` pins each one with
`try_acquire` and switches the pool to delete-on-zero.
Expand Down Expand Up @@ -260,5 +258,5 @@ How the code meets each rule:
| §5.3 IOCP | Conforms: socket and acceptor initiators re-check cancellation before `on_pending`, as the handle initiators do. IOCP is checked only by Windows CI. |
| §5.5 descriptor numbers | Conforms: posix file gate, uring flush before close or release, cancelled waits skip the probe on uring and the IOCP wait reactor. |
| §5.4 executor queues | Conforms: every awaitable owns its continuation; no op embeds one. |
| §6 teardown | Conforms on all backends, timers first everywhere; implemented per backend with no shared statement. Every service gives up its work in `shutdown()` and posts nothing from its destructor, so every frame is destroyed before any service (`testShutdownDestroysFramesBeforeServices` in `teardown_destroy.cpp`). |
| §6 teardown | Conforms on all backends. Implemented per backend with no shared statement. Every service gives up its work in `shutdown()` and posts nothing from its destructor, so every frame is destroyed before any service (`testShutdownDestroysFramesBeforeServices` in `teardown_destroy.cpp`). |
| §7 recycling | Mixed. Some `reuse()` methods assert fields others reset. |
2 changes: 1 addition & 1 deletion include/boost/corosio/delay.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -234,7 +234,7 @@ class clock_delay_awaitable
}

// Runs on the scheduler thread executing the completion op,
// before the continuation is posted, so the frame cannot die
// before the continuation is dispatched, so the frame cannot die
// concurrently.
static bool on_fire(void* ctx) noexcept
{
Expand Down
51 changes: 51 additions & 0 deletions include/boost/corosio/detail/scheduler.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
namespace boost::corosio::detail {

class scheduler_op;
class timer_service;

/** Define the abstract interface for the event loop scheduler.

Expand Down Expand Up @@ -156,7 +157,56 @@ class BOOST_COROSIO_DECL scheduler
cancels_posting_.fetch_sub(1, std::memory_order_release);
}

/** Attach the context's timer service.

The timer service is created with the context's first timer,
possibly on a thread other than the one running the loop; until
it is attached the loop has no timers to wait on.

@par Preconditions
No timer service is attached.

@par Exception Safety
Strong guarantee: on a throw, nothing is attached.

@param svc The service to attach.
*/
void attach_timer_service(timer_service& svc)
{
on_timer_service_attached(svc);
timer_svc_.store(&svc, std::memory_order_release);
}

/// Detach the timer service as it is destroyed.
void detach_timer_service() noexcept
{
timer_svc_.store(nullptr, std::memory_order_release);
}

protected:
/// Return the attached timer service, or null before the first timer.
timer_service* timers() const noexcept
{
return timer_svc_.load(std::memory_order_acquire);
}

/** Prepare to wait on a timer service about to be attached.

Called before the service is published to the loop, so the loop
never sees a service it cannot yet wait on. A scheduler that
cannot get a kernel timer falls back to bounding its wait by the
nearest expiry rather than throwing: the first timer is often
made inside an `await_suspend`, where a throw could only
terminate.

@par Exception Safety
Strong guarantee: a throw leaves the scheduler as it was, and
the next timer calls this again.

@param svc The service being attached.
*/
virtual void on_timer_service_attached(timer_service& svc) = 0;

/** Wait out the cancellers counted by @ref cancel_posting_begin.

Called before the shutdown drain, so it finds their posts; the
Expand All @@ -173,6 +223,7 @@ class BOOST_COROSIO_DECL scheduler
BOOST_COROSIO_MSVC_WARNING_PUSH
BOOST_COROSIO_MSVC_WARNING_DISABLE(4251)
std::atomic<std::size_t> cancels_posting_{0};
std::atomic<timer_service*> timer_svc_{nullptr};
BOOST_COROSIO_MSVC_WARNING_POP
};

Expand Down
4 changes: 2 additions & 2 deletions include/boost/corosio/detail/timer.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -427,10 +427,10 @@ struct BOOST_COROSIO_SYMBOL_VISIBLE waiter_node
/// The timer service that completes this waiter.
timer_service* svc_ = nullptr;

/// The continuation posted to resume the coroutine.
/// The continuation that resumes the coroutine.
capy::continuation cont_;

/// The executor the continuation is posted through.
/// The executor the coroutine resumes on.
capy::executor_ref d_;

// Borrowed from the awaiting chain's await_context, which outlives the
Expand Down
58 changes: 44 additions & 14 deletions include/boost/corosio/detail/timer_service.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ class scheduler;
pointer on waiter_node is used as a "still in list" marker.
A waiter_node's storage is the suspended coroutine's frame:
every completion path must finish touching the node before
posting the continuation or destroying the handle.
dispatching the continuation or destroying the handle.
*/

inline void timer_service_invalidate_cache() noexcept;
Expand Down Expand Up @@ -156,6 +156,9 @@ class BOOST_COROSIO_DECL timer_service final
timer_object_pool pool_;
callback on_earliest_changed_;
bool shutting_down_ = false;
// Set once the scheduler waits on this service; read without the
// mutex by every timer construction after the first.
std::atomic<bool> attached_{false};

// Cancellers that claimed a waiter and have yet to post it;
// shutdown waits them out so the scheduler's drain finds the post.
Expand All @@ -166,10 +169,21 @@ class BOOST_COROSIO_DECL timer_service final
BOOST_COROSIO_MSVC_WARNING_POP

public:
/// Construct the timer service bound to a scheduler.
inline timer_service(capy::execution_context&, scheduler& sched)
: sched_(&sched)
/** Construct the timer service for a context's scheduler.

Created with the context's first timer. It does not attach to
the scheduler here: a concurrent first use may construct a
second service that the context then discards. See
@ref attach.

@throws std::logic_error if the context has no scheduler.
*/
inline explicit timer_service(capy::execution_context& ctx)
: sched_(ctx.find_service<scheduler>())
{
if (!sched_)
detail::throw_logic_error(
"timer_service: the context has no scheduler");
}

/// Return the associated scheduler.
Expand All @@ -178,8 +192,32 @@ class BOOST_COROSIO_DECL timer_service final
return *sched_;
}

/// Destroy the timer service.
~timer_service() override = default;
/// Destroy the timer service, detaching it from the scheduler.
~timer_service() override
{
if (attached_.load(std::memory_order_acquire))
sched_->detach_timer_service();
}

/** Attach this service to the scheduler, if not yet attached.

Call once the context has settled which service it keeps; no
timer of this service can wait before this returns.

@par Exception Safety
Strong guarantee: on failure the service stays detached and
the next call retries.
*/
inline void attach()
{
if (attached_.load(std::memory_order_acquire))
return;
std::lock_guard lock(mutex_);
if (attached_.load(std::memory_order_relaxed))
return;
sched_->attach_timer_service(*this);
attached_.store(true, std::memory_order_release);
}

timer_service(timer_service const&) = delete;
timer_service& operator=(timer_service const&) = delete;
Expand Down Expand Up @@ -699,14 +737,6 @@ timer_service::swap_heap(std::size_t i1, std::size_t i2)
// in timer.hpp (e.g. code that only reaches timer.hpp through
// delay.hpp, without transitively including a scheduler header).

// Free functions

inline timer_service&
get_timer_service(capy::execution_context& ctx, scheduler& sched)
{
return ctx.make_service<timer_service>(sched);
}

} // namespace boost::corosio::detail

#endif
Loading
Loading