Skip to content

initThreadPool sometimes fails on main thread due to Atomics.wait in crossbeam-channel #36

@tjramage

Description

@tjramage

Problem

When calling initThreadPool from the browser's main thread, it intermittently fails with:

RuntimeError: Atomics.wait cannot be called in this context

This happens rarely (timing-dependent), but is reproducible.

Stack trace:

RuntimeError: Atomics.wait cannot be called in this context
  at app.wasm.core::core_arch::wasm32::atomic::memory_atomic_wait32::hf3aeb4fb59ceb28e (app_bg.wasm:0x5a40f8)
  at app.wasm.std::sys::pal::wasm::futex::futex_wait::hdf1f91fb42b62e65 (app_bg.wasm:0x48018f)
  at app.wasm.std::sys::sync::mutex::futex::Mutex::lock_contended::h1e76e655216bb9ac (app_bg.wasm:0x3a328f)
  at app.wasm.std::sys::sync::mutex::futex::Mutex::lock::hdac277faa407dd77 (app_bg.wasm:0x47a3b5)
  at app.wasm.std::sync::poison::mutex::Mutex<T>::lock::h19fee1e9e87f2935 (app_bg.wasm:0x51f5d2)
  at app.wasm.crossbeam_channel::waker::SyncWaker::notify::h7ca8267df8e13ce7 (app_bg.wasm:0x30d7e6)
  at app.wasm.crossbeam_channel::flavors::array::Channel<T>::write::h5eaa93aff2a3829a (app_bg.wasm:0x221e71)
  at app.wasm.crossbeam_channel::flavors::array::Channel<T>::send::h3ae2c3b3c3608368 (app_bg.wasm:0x23ccd7)
  at app.wasm.crossbeam_channel::channel::Sender<T>::send::h9e541c0f305ff8a1 (app_bg.wasm:0x3c5761)
  at app.wasm.wasm_bindgen_rayon::wbg_rayon_PoolBuilder::build::{{closure}}::h5621a25baf12822c (app_bg.wasm:0x50ddc3)
  overrideMethod    @    installHook.js:1
  start    @    client.js:15
  await in start
  (anonymous)    @    client.js:26

Cause

The thread pool initialization uses crossbeam-channel for coordination. When there's mutex contention in SyncWaker::notify (e.g., worker threads spinning up quickly), it calls std::sync::Mutex::lock_contended, which uses Atomics.wait. Obviously this is forbidden on the browser's main thread.

Why web_spin_lock doesn't help

Rayon 1.8.1 added the web_spin_lock feature to address Atomics.wait issues on the main thread, but this only affects rayon's internal mutexes. The problematic code path is in crossbeam-channel, which uses std::sync::Mutex directly.

Current workaround

Wrapping initThreadPool in a retry loop with a small delay:

async function initThreadPoolWithRetry(numThreads, maxRetries = 5) {
  for (let i = 0; i < maxRetries; i++) {
    try {
      await initThreadPool(numThreads)
      return
    } catch (e) {
      if (e.message?.includes('Atomics.wait') && i < maxRetries - 1) {
        await new Promise(r => setTimeout(r, 25))
        continue
      }
      throw e
    }
  }
}

Potential solutions?

  • Use a wasm-compatible channel implementation that spin-waits instead of blocking on the main thread
  • Document this limitation and recommend the retry workaround
  • Explore whether the pool setup coordination could avoid the contended mutex path

Environment

  • wasm-bindgen-rayon: 1.3.0
  • rayon: 1.11.0 (with web_spin_lock enabled)
  • Browser: Chrome

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions