Skip to content

std.mutex

std.mutex provides Mutex<T>, the mutual-exclusion escape hatch for data that is not lock-free-atomic-eligible: it owns its T and hands out exclusive access under a lock. It is the type the Atomic<T> "not lock-free" diagnostic points at. Access is either scoped — run a body with the lock held, released automatically on exit — or through an RAII guard whose drop unlocks. For lock-free primitives use std.atomic; for the concurrency model see Concurrency And Parallelism.

Quick Example

Zynx
import std.mutex;

fn main() {
    var m = mutex.new<i32>(0);

    // Scoped access: the lock is held for the body, released on return.
    m.with((v) => { v = 42; });
    m.read((v) => { assert v == 42; });

    // RAII guard: lock() returns a guard; get() borrows the value; drop unlocks.
    {
        var g = m.lock();
        g.get() = *g.get() + 1;
    }

    m.read((v) => { assert v == 43; });
}

Contract

  • mutex.new<T>(value) mints an ^Mutex<T> owning value. The lock object is heap-boxed behind an opaque handle, so a Mutex<T> value may move freely before it is first locked; while a guard is live the borrow checker forbids moving it.
  • Mutex<T> is move-only. Because acquiring the lock mutates through &self, the binding must be var (see Sharing for the cross-thread story).
  • There is no poisoning. A panic aborts the process, so there is nothing to observe a poisoned state; and an error returned with throws from inside a scoped body still runs the release (scoped access unlocks via defer, the guard via drop), so the lock is released and the data stays consistent on the error path. lock is infallible; try_with reports contention.
  • The mutex is not reentrant: locking it again on the same thread while it is already held deadlocks. The borrow checker catches the common case (a second lock while the first guard is live is a borrow conflict).

Scoped Access

with, read, and try_with run a caller-supplied body with the lock held and release it when the body returns — the simplest and hardest-to-misuse form.

  • with(body) runs body with a mutable borrow of the inner value.
  • read(body) runs body with a shared bare borrow.
  • try_with(body) acquires the lock without blocking: it returns true and runs body if the lock was free, or false if it was contended.
Zynx
import std.mutex;

fn main() {
    var m = mutex.new<i32>(10);

    let ran = m.try_with((v) => { v = *v + 5; });
    assert ran;
    m.read((v) => { assert v == 15; });
}

The RAII Guard

lock() acquires the lock and returns a MutexGuard<T> that holds it. The guard borrows the mutex for a region tied to the enclosing scope, so:

  • a second lock() while the guard is live is rejected (you cannot alias the locked mutex);
  • the guard cannot outlive the mutex;
  • the lock is released when the guard is dropped — at the end of its scope.

get() returns a mutable borrow of the guarded value, tied to the guard's region; mutating through it requires the guard binding to be var. Scope the guard tightly (an inner { ... } block) to bound how long the lock is held.

Sharing

Mutex<T> is the mutual-exclusion primitive together with its memory-ordering guarantee: acquiring the lock synchronizes-with the previous release, so data written under the lock is visible to the next holder. What it does not yet provide is a cross-thread sharing mechanism. Mutex<T> carries a drop, and a type with a user drop is currently rejected at a worker boundary, so a Mutex can be neither shared by borrow nor moved into a worker in this release. Multi-owner sharing (the Arc<Mutex<T>> pattern) is future work. Today the type is used for exclusion and interior mutability within a thread; for communication between tasks, use a channel.

Ownership, Allocation, And Errors

new heap-allocates the OS lock object once; Mutex<T> owns and drops its T through normal field cleanup, and destroying the lock is handled by the mutex's own drop. Scoped access and lock/get do not allocate. On single-threaded SDK targets without threads the lock degrades to a no-op (there is no contention to guard).

Use std.atomic for lock-free primitives, and std.channel for message passing instead of shared mutable state. See Concurrency And Parallelism for the runtime model and Standard Library Inventory for the full exported surface.

API Reference

Types

TypeKindDescription
Mutex<T>structMutual-exclusion cell owning a T. Move-only.
MutexGuard<T>structRAII lock guard; get() borrows the value, drop unlocks.

Functions

FunctionSignatureReturnsDescription
newnew<T>(value: ^T) -> ^Mutex<T>^Mutex<T>Construct a mutex owning value.

Mutex Methods

MethodSignatureReturnsDescription
withwith(&self, body: (&T) -> void)Run body with a mutable borrow of the value under the lock.
readread(&self, body: (T) -> void)Run body with a shared borrow of the value under the lock.
try_withtry_with(&self, body: (&T) -> void) -> boolboolNon-blocking with; true if the lock was acquired, false if contended.
locklock(&self) -> ^MutexGuard<T>^MutexGuard<T>Acquire the lock and return a guard that releases it on drop.

Guard Methods

MethodSignatureReturnsDescription
getget(self) -> &T&TMutable borrow of the guarded value, tied to the guard's region.

(Region and closure-region annotations are elided from the signatures above for readability; the guard's borrow is tied to the enclosing scope.)

Status

std.mutex is the current public mutual-exclusion API: Mutex<T> with scoped (with/read/try_with) and RAII-guard (lock/get) access over a pthread lock — scoped bodies unlock via defer, the guard via drop — with no poisoning. Shared-ownership across many threads (Arc<Mutex<T>>) and auto-deref guard access (g.field without get()) are not part of this release. For the full exported surface, see Standard Library Inventory.

Released under the MIT License.