Skip to content

Prepare shared code for cooperative single-threaded event backends - #5

Open
scottmarchant wants to merge 1 commit into
mainfrom
prepare-cooperative-event-backend
Open

scottmarchant wants to merge 1 commit into
mainfrom
prepare-cooperative-event-backend

Conversation

@scottmarchant

@scottmarchant scottmarchant commented Aug 17, 2026

Copy link
Copy Markdown
Collaborator

Re-scoped 2026-09-04. Upstream review on swiftlang#953 asked for a host-loop model instead of drain on poke. #8 built it, and #3 now carries it. This slice is now the shared main-queue drain hoist only: 14 lines added, 1 removed, in one file. The poke-defer hooks and the sync funnel split are gone. The text below predates the re-scope where it describes those hooks. This branch mirrors the upstream PR commit for commit.

Staging note: this draft lives in the PassiveLogic fork for internal review. Progress for the full series is tracked in #3. When this draft is ready, we will cherry-pick its commits onto a new branch named feat/prepare-cooperative-event-backend and open the upstream PR from that branch. The separate branch lets us keep changing and pre-testing this draft here, independent of the upstream PR. This staging note is the only fork-specific content; the rest of this description is written for the upstream PR.

Summary

This PR prepares shared code for platforms that run libdispatch on one thread, with no worker threads. It adds two things: no-op hook macros around specific critical sections, and one shared main-queue drain function. It makes no functional change on any current platform. Linux builds and test results are identical before and after this change.

Context

WebAssembly (wasm32-wasip1) runs libdispatch on a single thread. That platform has no way to create a worker thread. A port for it therefore uses a cooperative event backend: when work is enqueued and nothing is running, the backend runs the work immediately, on the thread that enqueued it.

We built and tested that port as a series of small, independent changes. This PR is the first change in the series. Later changes add the WASI build system, the cooperative event backend, the Swift overlay, and file-descriptor and signal event sources. The later changes touch shared files only inside wasm-only blocks; they do not change code that other platforms compile.

You do not need the later changes to review this one. The two edits here are self-contained. Each is safe on its own, and each is required before a cooperative backend can exist.

What changes

1. Poke-defer hooks. The macros _dispatch_cooperative_pokes_defer() and _dispatch_cooperative_pokes_undefer() compile to ((void)0) on every current platform. A cooperative event backend defines the real versions and declares its functions when it arrives; this PR pre-stages no declarations. The hooks bracket each critical section that can enqueue work while it holds an internal lock. Each site carries a one-line comment; this list is the long form:

  • the dispatch_sync and dispatch_barrier_sync inline funnels: on the inline paths, the client callout runs with the queue's barrier lock held
  • both dispatch_async_and_wait funnels, including the private-data block entry that Swift's asyncAndWait(execute:) produces: the invoke can run inline with the acquired width or barrier held
  • _dispatch_barrier_trysync_or_async_f: the invoke runs with the barrier lock held
  • dispatch_once initializers: the initializer runs with the once gate held, and a drained item that re-enters the same dispatch_once would crash where threaded platforms simply wait
  • object dispose: dispose submits detached work (queue-specific destructor batches) while the object is partially torn down
  • the specifics-hash mutation in dispatch_queue_set_specific: the destructor push must not run the client destructor under dqsh_lock

On a threaded platform, a poke only wakes another worker, so the hooks change nothing. On a cooperative backend, the hooks defer the inline execution until the outermost section exits. Without the hooks, the enqueued work would run under the caller's lock. A program that is correct on every threaded platform would then deadlock.

2. A shared main-queue drain. _dispatch_main_queue_drain moves out of DISPATCH_COCOA_COMPAT into its own guard. The two runloop-only steps stay gated under DISPATCH_COCOA_COMPAT: the runloop-handle initialization, and the thread-QoS override propagation. A cooperative backend can then drain the thread-bound main queue through the same code the CFRunLoop callback uses. The alternative is a 63-line divergent copy, which we had, and which drifts.

Why this is safe

  • The hook macros compile away on every current platform.
  • The sync funnel split (an _impl function plus a wrapper) is always-inline. The compiler folds it back together.
  • The hoisted drain compiles only where it compiled before. The compiled result on Darwin is unchanged, because both gated steps sit under a macro that is always true there.
  • A Linux container build shows main and this branch produce identical results: both pass the same 23 of 23 upstream tests.
  • The complete cooperative backend, built on these exact seams, passes 52 of 52 tests on the combined branch (feat: Full combined set of changes to add Wasm support to libDispatch #3). That suite includes a regression test for each hook site.

What we ask from reviewers

The public macOS CMake build of this repository does not currently configure, so we cannot compile the DISPATCH_COCOA_COMPAT path ourselves. We ask for one Apple CI run to confirm the Darwin build is unchanged. That is the main risk this PR carries, and it is the reason we send this small change first.

Before we upstream

This section is fork-only, like the staging note at the top. It lists the tasks that block the upstream submission of this PR. The full series tracker lives in #3.

  • @scottmarchant Investigate the threads wasm SDK (wasm32-wasip1-threads) and prove the concept. DONE on branch feat/scottm/libDispatchWasmThreads: libdispatch becomes multi-threaded (dispatch_async on real worker pthreads under wasmtime v24), and the seams in this PR need no change and no API signature moves. This PR is unblocked.
  • @krodak @scottmarchant Complete the internal review of this draft. DONE: approved 2026-08-18; the review edits (one-line hook comments, dropped pre-staged declarations, this description) landed after the approval.
  • @scottmarchant Cherry-pick onto feat/prepare-cooperative-event-backend, open the upstream PR, and raise the Apple CI question. DONE 2026-08-20: one squashed commit, upstream draft PR feat: prepare shared code for cooperative single-threaded event backends swiftlang/swift-corelibs-libdispatch#953. The CI ask sits in the PR body; it goes live when the draft is marked ready. Cherry-pick notes from review: squash to one commit; say "funnels" in the message (it covers both dispatch_async_and_wait funnels); spell out fork links as PassiveLogic/swift-corelibs-libdispatch#N; no tool trailer on the upstream commit.
  • Research the blast radius in swiftlang repos where #if canImport(Dispatch) is mis-used: find the WASI code paths that become wrong, or break, when real Dispatch support for wasm rolls out.

Comment thread src/event/event_internal.h Outdated
Comment thread src/queue.c Outdated
@krodak

krodak commented Aug 18, 2026

Copy link
Copy Markdown
Collaborator

A few small things for the cherry-pick, none of them code:

  • squash to one commit, and the message says "funnel" where it now covers both funnels
  • the "feat: Full combined set of changes to add Wasm support to libDispatch #3" in the test claim will link to some ancient unrelated issue on swiftlang, better to spell it out and add a real link when we open it there
  • "none of the later changes touch shared code again" isn't quite true, later slices do touch shared files, just inside wasm-only blocks, worth wording it that way
  • the Claude trailer most likely shouldn't travel upstream 😄

@krodak
krodak self-requested a review August 18, 2026 18:54
@scottmarchant

Copy link
Copy Markdown
Collaborator Author

Threads research result. The branch feat/scottm/libDispatchWasmThreads proves multi-threaded dispatch on wasm32-unknown-wasip1-threads. With an experimental DISPATCH_WASI_THREADS=ON CMake knob, libdispatch builds for the threads triple and runs the upstream threaded shape: a pthread worker pool, blocking waits on a wasm futex, POSIX semaphores, and a manager thread on a condition variable. Under wasmtime v24 (-S threads) the smoke test prints PASS: async=8/8 on-worker-thread=8/8 sync=1 timer=1.

The changes in this PR need no modification to support it. In threads mode the poke-defer hooks compile to ((void)0), exactly as on every threaded platform, and the shared main-queue drain compiles only where it compiled before. No hook site moves and no API signature changes. The threads build exercises these exact seams and builds clean with -Werror; the cooperative build is unchanged at 52 of 52 tests.

Full findings (toolchain facts, the gate audit, runtime support, what stays unimplemented) are in tests/wasm/THREADS-RESEARCH.md on that branch, and the series tracker note is in #3.

scottmarchant added a commit that referenced this pull request Aug 19, 2026
Proven end to end on this machine: a 4-thread pthread program with
atomics, semaphores, and accurate timed waits compiles with both the
wasi-sdk 33 clang and the Swift 6.3.3 host clang against the
swift-wasm-6.3-RELEASE threads artifactbundle, and passes under
wasmtime v24 LTS with -S threads.

Key findings:
- _REENTRANT discriminates the threads triple at compile time.
- Memory must be imported and shared via explicit link flags, or
  pthread_create fails with EAGAIN at runtime.
- Current wasmtime (v47) has removed wasi-threads; v24 LTS is the
  one solid runtime today. The proposal is deprecated upstream, so
  threads mode should be an experimental knob, not the default.
- The slice A seams need no change for threads mode: the poke-defer
  hooks compile to no-ops there, and no API signature moves. This
  answers the #5 pre-upstream blocker.
- Full gate audit of every __wasi__ conditional with a KEEP / SPLIT /
  THREADS classification, and a threads-mode design sketch.

Co-authored-by: Krzysztof Rodak <krodak.konta@gmail.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@scottmarchant

Copy link
Copy Markdown
Collaborator Author

Note, my research found some concerns with #if canImport(Dispatch) issues in swift-nio, but this MR won't affect that. I moved the research note to the larger combined #3

@scottmarchant
scottmarchant marked this pull request as ready for review August 20, 2026 04:28
A single-threaded WASI backend drains the main queue when the host
event loop calls back into libdispatch. _dispatch_main_queue_drain
compiles only under DISPATCH_COCOA_COMPAT, next to the CFRunLoop
callback.

Move _dispatch_main_queue_drain under a separate guard. The two
runloop-only steps stay under DISPATCH_COCOA_COMPAT: the runloop-handle
initialization and the thread-QoS override propagation. The guard also
names __wasi__. That arm compiles nothing here.

Darwin builds the same code as before. Platforms that define neither
macro compile neither version, as before.

Co-authored-by: Krzysztof Rodak <krodak.konta@gmail.com>
@scottmarchant
scottmarchant force-pushed the prepare-cooperative-event-backend branch from d352c83 to ba10c29 Compare September 6, 2026 05:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants