From 4f16e80ceadc19c50c5be67dd9cc2c2f9aa4beb8 Mon Sep 17 00:00:00 2001 From: Matthew Lugg Date: Thu, 15 Jan 2026 00:27:26 +0000 Subject: [PATCH] std: halve the number of mutexes per mutex On NetBSD and Illumos, we were using the `std.Thread.Futex`-based implementation of `std.Thread.Mutex`. But since futex is not a primitive on these targets, the implementation of `std.Thread.Futex` was based on pthread primitives, including `pthread_mutex_t`. This had the amusing consequence that locking a contended mutex on NetBSD would actually perform 2 mutex locks, 2 mutex unlocks, and 1 condition wait; likewise, unlocking a contended mutex would perform 2 mutex locks, 2 mutex unlocks, and 1 condition signal. Having read some cutting-edge studies, I have concluded that this is a slightly suboptimal approach. Instead, let's just use pthread mutexes directly in this case; that's an obviously better idea. In the future, I think we can probably entirely remove our usages of pthread sync primitives---no platform actually treats them as the base primitives. Of the platforms which std has any meaningful support for today, most support futexes, and the exceptions (NetBSD and Illumos) support a thread parking API. We can implement futex and/or mutex on top of thread parking and drop the pthread dependency entirely. --- lib/std/Thread/Mutex.zig | 63 +++++++++++++++++++++++++++++++++++----- 1 file changed, 55 insertions(+), 8 deletions(-) diff --git a/lib/std/Thread/Mutex.zig b/lib/std/Thread/Mutex.zig index 392fcde42a..eb0dd42ceb 100644 --- a/lib/std/Thread/Mutex.zig +++ b/lib/std/Thread/Mutex.zig @@ -45,14 +45,30 @@ const Impl = if (builtin.mode == .Debug and !builtin.single_threaded) else ReleaseImpl; -const ReleaseImpl = if (builtin.single_threaded) - SingleThreadedImpl -else if (builtin.os.tag == .windows) - WindowsImpl -else if (builtin.os.tag.isDarwin()) - DarwinImpl -else - FutexImpl; +const ReleaseImpl = Impl: { + if (builtin.single_threaded) break :Impl SingleThreadedImpl; + if (builtin.os.tag == .windows) break :Impl WindowsImpl; + if (builtin.os.tag.isDarwin()) break :Impl DarwinImpl; + + if (builtin.target.os.tag == .linux or + builtin.target.os.tag == .freebsd or + builtin.target.os.tag == .openbsd or + builtin.target.os.tag == .dragonfly or + builtin.target.cpu.arch.isWasm()) + { + // Futex is the system's synchronization primitive; use that. + break :Impl FutexImpl; + } + + if (std.Thread.use_pthreads) { + // This system doesn't have a futex primitive, so `std.Thread.Futex` is using `PosixImpl`, + // which implements futex *on top of* pthread mutexes and conditions. Therefore, instead + // of going through that long inefficient path, just use pthread mutex directly. + break :Impl PosixImpl; + } + + break :Impl FutexImpl; +}; const DebugImpl = struct { locking_thread: std.atomic.Value(Thread.Id) = std.atomic.Value(Thread.Id).init(0), // 0 means it's not locked. @@ -208,6 +224,37 @@ const FutexImpl = struct { } }; +const PosixImpl = struct { + mutex: std.c.pthread_mutex_t = .{}, + + fn tryLock(impl: *PosixImpl) bool { + switch (std.c.pthread_mutex_trylock(&impl.mutex)) { + .SUCCESS => return true, + .BUSY => return false, + .INVAL => unreachable, // mutex is initialized correctly + else => unreachable, + } + } + + fn lock(impl: *PosixImpl) void { + switch (std.c.pthread_mutex_lock(&impl.mutex)) { + .SUCCESS => return, + .INVAL => unreachable, // mutex is initialized correctly + .DEADLK => unreachable, // not an error checking mutex + else => unreachable, + } + } + + fn unlock(impl: *PosixImpl) void { + switch (std.c.pthread_mutex_unlock(&impl.mutex)) { + .SUCCESS => return, + .INVAL => unreachable, // mutex is initialized correctly + .PERM => unreachable, // not an error checking mutex + else => unreachable, + } + } +}; + test "smoke test" { var mutex = Mutex{};