commit 4f16e80ceadc19c50c5be67dd9cc2c2f9aa4beb8 (tree)
parent c518593e9793a2aed4e0173348aff2cbef58b717
Author: Matthew Lugg <mlugg@mlugg.co.uk>
Date: Thu, 15 Jan 2026 00:27:26 +0000
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.
Diffstat:
1 file changed, 55 insertions(+), 8 deletions(-)
diff --git 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{};