Use Locks and ThreadPool

Preserve monitor semantics without losing C++ cleanup and callback lifetime safety.

AdvancedThreadingSource-checked

Choose the right layer

Threading contains threads, monitor-style synchronization, locks, events, semaphores, cancellation primitives, and ThreadPool. Native C++ synchronization remains available too. Use a Sharp Runtime type when ported behavior matters; use an idiomatic standard primitive when no compatibility contract is required.

set(SHARP_RUNTIME_COMPONENTS Threading)
target_link_libraries(app PRIVATE SharpRuntime::Threading)

Wrap Monitor in RAII

Monitor::Enter/Exit model C#’s reentrant lock monitor, but C++ exceptions make a hand-written exit at the bottom unsafe. A tiny guard restores lexical cleanup:

#include <System/Threading/Monitor.hpp>

class MonitorGuard {
    const void* key_;
public:
    explicit MonitorGuard(const void* key) : key_(key) {
        System::Threading::Monitor::Enter(key_);
    }
    ~MonitorGuard() {
        System::Threading::Monitor::Exit(key_);
    }
    MonitorGuard(const MonitorGuard&) = delete;
    MonitorGuard& operator=(const MonitorGuard&) = delete;
};

struct Counter {
    int value{};
    void Increment() {
        MonitorGuard guard(this);
        ++value;
    }
};

The runtime registry keys monitors by pointer identity and keeps each registry entry for the life of the process. Do not lock on a temporary address, an address that may be reused for an unrelated object, or a pointer whose meaning changes while other threads retain it.

Understand reentrancy, wait, and pulse

The backing mutex is recursive. The same thread may enter repeatedly and must exit the same number of times. Wait requires ownership, releases the complete recursion depth, blocks, and restores that depth after reacquiring. Pulse and PulseAll also require ownership.

// Always wait in a predicate loop: wake-up is permission to re-check,
// not proof that the condition is now true.
MonitorGuard guard(&state);
while (!state.ready) {
    System::Threading::Monitor::Wait(&state);
}

The snippet is a behavioral pattern: an application guard that calls Wait must remain logically associated with the monitor while Wait temporarily releases and restores it. Never hold an unrelated native mutex while blocking on this monitor unless lock ordering is documented.

Queue work with honest expectations

#include <System/Threading/ThreadPool.hpp>
#include <atomic>

std::atomic<bool> finished{false};
System::Threading::ThreadPool::QueueUserWorkItem([&finished] {
    // Perform bounded work and catch/report failures here.
    finished.store(true, std::memory_order_release);
});

At the selected source revision, QueueUserWorkItem creates and detaches a native thread for each item. It is not a bounded reusable worker pool, does not provide a completion handle, and does not retain captured references safely. The reported min/max settings are process-global configuration values; changing them does not turn the detached implementation into .NET’s adaptive scheduler.

A reference capture is a lifetime contract

The example keeps finished alive until work completes. In real code, prefer captured values or shared_ptr-owned state and add an explicit completion signal. Never let a detached callback outlive stack variables, services, logging backends, or shutdown state it borrows.

Handle failure inside detached work

An exception escaping a raw detached thread invokes std::terminate. Put the error boundary inside the callback, publish the result through owned shared state, and make shutdown wait for that state. If completion and exception propagation matter, Task is normally a better abstraction than fire-and-forget ThreadPool work.

Emscripten needs pthreads

On an Emscripten build without pthreads, queueing work throws PlatformNotSupportedException. Source availability is not runtime evidence: browser isolation headers, worker configuration, and deployment server headers also influence whether a pthread-enabled artifact works.

C# comparison

C# / .NETSharp RuntimeImportant difference
lock (obj)Monitor::Enter/Exit plus RAII guardNo compiler-generated finally; pointer identity backs the registry.
Managed thread poolDetached thread per queued itemNo bounded shared pool or queue-pressure contract.
GC roots callback capturesC++ capture rulesBorrowed references can dangle.
Unhandled task exceptions remain task stateDetached callback exceptions terminateCatch inside the callback or use Task.

Verification checklist

  • Use RAII for every acquired monitor, mutex, semaphore permit, and thread join.
  • Write predicate-loop tests for wait/pulse and timeout paths.
  • Prove captured state outlives detached work and shutdown observes completion.
  • Exercise callback exceptions without allowing them to cross a native thread entry point.
  • Use ThreadSanitizer where supported; passing a functional test does not prove absence of a data race.