feat: add opt-in synchronous future bridge - #153
Merged
Conversation
Provide a minimal single-future blocking executor behind the block_on Cargo feature, with block_on, block_on_timeout, and FutureExt methods.
# Conflicts: # CHANGELOG.md # README.md # asyncband/Cargo.toml # asyncband/src/lib.rs # xtask/src/main.rs
Keep only block_on and FutureExt, and give each invocation an isolated parking signal so it cannot consume the current thread's shared park token.
Inline the private parker so the blocking feature stays dependency-free while preserving independent notification tokens.
tisonkun
approved these changes
Aug 19, 2026
tisonkun
left a comment
Member
There was a problem hiding this comment.
Thanks for starting this contribution. The original revision established the feature-gated, single-future synchronous bridge, including indefinite and timed waits plus the FutureExt calling style. I updated the contributor branch in place and am approving the resulting design.
For clarity, these are the maintainer-side changes relative to the original author revision:
- Updated the branch to current
main, renamed the module/feature fromblock_ontoblocking, and documented it as synchronous interoperability rather than an async primitive. - Reduced the public surface from free
block_on/block_on_timeoutfunctions, matching extension methods, and a publicTimeouttype to onlyFutureExt::block_onandFutureExt::wait_timeout. UFCS (FutureExt::block_on(future)) still provides function syntax without a second entry point. A timeout now returnsNoneand explicitly drops/cancels the consumed future. - Replaced the direct
std::thread::parktoken with an isolated three-state parker. The parker/waker pair is cached per thread, recursive waits receive a fresh pair, and unrelated thread parking cannot consume its notifications. - Inlined the small required parker state machine instead of adding a normal dependency. Ready and early-notification paths are atomic;
MutexandCondvarare used only when the thread actually sleeps. - Moved observable API coverage to integration tests, replaced sleep-based coordination with deterministic wakeups where practical, and added regressions for nested waits, unrelated thread park tokens, timeout expiry, and oneshot cancellation. Public composition and Divan benchmark coverage were added as well.
- Reworked the README, rustdoc, attribution, and Changelog around the final released API rather than the intermediate commit history.
The feature matrix, stable and Rust 1.86 test suites, lint/rustdoc checks, benchmarks, and Linux/macOS/Windows CI all pass on the final revision.
tisonkun
enabled auto-merge (squash)
August 19, 2026 14:32
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Add an opt-in
asyncband::blocking::FutureExtinteroperability utility for synchronous Rust code that needs to wait on one runtime-agnostic future, either indefinitely or for a bounded duration.Closes #147.
Public API
The
blockingCargo feature is disabled by default and has no normal dependencies. Its complete public waiting surface consists of twoFutureExtmethods:FutureExt::block_on() -> Outputfor an indefinite wait.FutureExt::wait_timeout(Duration) -> Option<Output>for a bounded wait.There is no separate free-function entry point. Callers that prefer function syntax can use UFCS—
asyncband::blocking::FutureExt::block_on(future)—which invokes the same trait method rather than maintaining a second API.The utility remains separate from the async primitive taxonomy: it bridges synchronous callers to async futures, but it is not itself a synchronization primitive or general-purpose runtime.
There is also no public timeout type.
wait_timeoutfollows the standard action-plus-_timeoutnaming used by APIs such asrecv_timeoutandpark_timeoutand usesOptionto keep the generic cancellation boundary explicit and compact.Timeout semantics
wait_timeoutconsumes the future and measures the timeout withstd::time::Instant:Duration::ZERO.Some(output)means the future completed;Nonemeans the deadline elapsed.Nonedrops the future. For an Asyncband oneshot receiver, that disconnects the receiving endpoint rather than leaving it available for another receive attempt.Future::poll, and the bridge still does not provide runtime timer, I/O, or task drivers.The parker's timed wait is interruptible through the same waker used by the indefinite path; after either a notification or a spurious wake, the future is polled again before deciding whether the deadline has elapsed.
Parking design
The implementation uses a private three-state notification token (
EMPTY,PARKED, andNOTIFIED) adapted fromparking2.2.1. The source attribution and upstream copyright are retained. The atomic token handles notifications delivered before sleep without a lock;MutexandCondvarare entered only when the calling thread actually needs to sleep.The parker and waker are cached per thread, following futures-lite. A recursive wait cannot borrow the active cached pair, so it creates a fresh pair and cannot steal the outer call's notification.
The implementation does not call
std::thread::parkdirectly. AThreadhas one shared notification token, so nested blocking calls or unrelated code parking the same thread can consume or coalesce a wake intended for this bridge. Pollster chose that tradeoff for its lock-free implementation, and its history records both the performance motivation and this correctness limitation:Adding a private atomic flag around
Thread::parkwould not close the race: a wake can arrive after the flag check and then have its shared thread token consumed by an unrelated park before this bridge parks. An independent waiting token is required for isolation.cargo tree -p asyncband --no-default-features --features blocking --edges normalcontains onlyasyncbanditself.Performance
The first private implementation allocated a
Mutex<bool>/Condvarparker per call and measured 21.42 ns for a ready future and 85.07 ns for a self-waking future. The cached atomic fast path removes that allocation and lock from both cases.A same-binary local Divan comparison against Pollster 1.0.1 after the trait-only API cleanup (1,000 samples) measured:
Thread::parkvia PollsterOn this machine, the direct-thread design saves about 1.2 ns for an immediately ready future and about 0.46 us when an actual cross-thread sleep occurs; the private parker is slightly faster in the self-wake case. Those differences do not justify exposing Asyncband to the shared-token deadlocks above. A permanent
ready_with_timeoutbenchmark also records theInstant/deadline overhead of the bounded API (16.77 ns median in the same run).Tests
FutureExt::block_on, ready-before-zero-timeout behavior, and actual timeout expiry.Mutexandoneshot.Branch update
Merged the latest
main, including #158, into the existing contributor branch without rewriting its published history.Validation
cargo x checkcargo x testcargo +1.86.0 x testcargo +nightly x lintcargo x benchcargo tree -p asyncband --no-default-features --features blocking --edges normal