Skip to content

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

Open
scottmarchant wants to merge 3 commits into
mainfrom
prepare-cooperative-event-backend
Open

Prepare shared code for cooperative single-threaded event backends#5
scottmarchant wants to merge 3 commits into
mainfrom
prepare-cooperative-event-backend

Conversation

@scottmarchant

@scottmarchant scottmarchant commented Aug 17, 2026

Copy link
Copy Markdown
Collaborator

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: WASI port V2 — combined branch: #2's runtime + #1's test scenarios + API-contract tests #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. 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.

scottmarchant and others added 2 commits August 17, 2026 17:49
…ends

Some platforms run libdispatch on one thread, with no worker threads.
An event backend for such a platform cannot wake a worker when work is
enqueued. Instead, a poke may run the enqueued work immediately, on the
thread that enqueued it. This commit prepares the shared code for that
execution model. It makes no functional change on any current platform.

Part 1: poke-defer hooks. The macros
_dispatch_cooperative_pokes_defer() and
_dispatch_cooperative_pokes_undefer() compile to ((void)0) unless a
cooperative event backend defines the real versions. The hooks bracket
each critical section that can enqueue work while it holds an internal
lock:

- the dispatch_sync and dispatch_barrier_sync inline funnels
- the dispatch_async_and_wait funnel
- _dispatch_barrier_trysync_or_async_f
- dispatch_once initializers (the once gate is held)
- object dispose (destructor batches are enqueued during teardown)
- the specifics-hash mutation in dispatch_queue_set_specific

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 them, enqueued
work would run under the caller's lock and deadlock.

Part 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
(the runloop-handle initialization and the thread-QoS override
propagation) stay gated under DISPATCH_COCOA_COMPAT. A cooperative
backend can then drain the thread-bound main queue through the same
code the CFRunLoop callback uses, instead of a divergent copy. The
compiled result on Darwin is unchanged. Platforms that define neither
macro compile neither version, as before.

Co-authored-by: Krzysztof Rodak <krodak.konta@gmail.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The private-data block entry that Swift's asyncAndWait(execute:)
produces does not pass through _dispatch_async_and_wait_f. It funnels
through _dispatch_async_and_wait_block_with_privdata and reaches
_dispatch_async_and_wait_recurse directly. Bracket that path with the
same poke-defer hooks the other funnels use. Without this, a
cooperative backend would run enqueued work under the caller's lock on
this one path.

Co-authored-by: Scott Marchant <15382220+scottmarchant@users.noreply.github.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@scottmarchant
scottmarchant force-pushed the prepare-cooperative-event-backend branch from d85fd0d to 4677b9c Compare August 17, 2026 23:49
@krodak
krodak force-pushed the prepare-cooperative-event-backend branch from 268b9d1 to 4677b9c Compare August 18, 2026 14:46
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: WASI port V2 — combined branch: #2's runtime + #1's test scenarios + API-contract tests #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.

Trim each poke-defer site comment to one line; the per-site rationale
moves to the PR description. Drop the WASI function declarations from
event_internal.h: nothing in this change defines them, and the event
backend change brings the complete declaration block together with
its implementation.

Linux container check after the edits: main and this branch still
pass the identical 23 of 23 upstream tests.

Co-authored-by: Krzysztof Rodak <krodak.konta@gmail.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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
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