Zig Mutexes
A mutex is a lock that lets only one thread at a time touch shared data. A thread locks the mutex, updates the data, and then unlocks it.
Two threads share one total
Each worker locks the mutex, adds 10, and unlocks. The main thread joins both workers, then prints the total.
const std = @import("std");
fn addTen(total: *i32, m: *std.Thread.Mutex) void {
m.lock();
total.* += 10;
m.unlock();
}
pub fn main() !void {
var out = std.io.getStdOut().writer();
var total: i32 = 0;
var m = std.Thread.Mutex{};
var t1 = try std.Thread.spawn(.{}, addTen, .{ &total, &m });
var t2 = try std.Thread.spawn(.{}, addTen, .{ &total, &m });
t1.join();
t2.join();
try out.print("total={d}\n", .{total});
}One lock guards two values
Each worker updates a total and a count under the same lock. The two values stay consistent because no worker can slip between them.
const std = @import("std");
fn record(total: *i32, count: *i32, m: *std.Thread.Mutex, amount: i32) void {
m.lock();
total.* += amount;
count.* += 1;
m.unlock();
}
pub fn main() !void {
var out = std.io.getStdOut().writer();
var total: i32 = 0;
var count: i32 = 0;
var m = std.Thread.Mutex{};
var t1 = try std.Thread.spawn(.{}, record, .{ &total, &count, &m, 12 });
var t2 = try std.Thread.spawn(.{}, record, .{ &total, &count, &m, 18 });
t1.join();
t2.join();
try out.print("total={d} count={d}\n", .{ total, count });
}Unlock with defer
Defer unlocks the mutex when the worker returns. The update still happens under the lock.
const std = @import("std");
fn addFive(total: *i32, m: *std.Thread.Mutex) void {
m.lock();
defer m.unlock();
total.* += 5;
}
pub fn main() !void {
var out = std.io.getStdOut().writer();
var total: i32 = 0;
var m = std.Thread.Mutex{};
var t1 = try std.Thread.spawn(.{}, addFive, .{ &total, &m });
var t2 = try std.Thread.spawn(.{}, addFive, .{ &total, &m });
t1.join();
t2.join();
try out.print("total={d}\n", .{total});
}Example
const std = @import("std");
fn record(balance: *i32, updates: *i32, m: *std.Thread.Mutex, amount: i32) void {
m.lock();
balance.* += amount;
updates.* += 1;
m.unlock();
}
pub fn main() !void {
var out = std.io.getStdOut().writer();
var balance: i32 = 0;
var updates: i32 = 0;
var m = std.Thread.Mutex{};
const amounts: [3]i32 = .{ 100, 50, 25 };
var threads: [3]std.Thread = undefined;
for (0..3) |i| {
threads[i] = try std.Thread.spawn(.{}, record, .{ &balance, &updates, &m, amounts[i] });
}
for (threads) |t| {
t.join();
}
try out.print("balance={d} updates={d}\n", .{ balance, updates });
}Key points
- Mutex: a mutex is a lock that lets only one thread at a time touch shared data.
- Lock: a lock is a claim on a mutex, and a thread waits for the claim if another thread holds it.
- std.Thread.Mutex is the mutex type, and a value of it starts unlocked.
- A thread calls lock before touching shared data and unlock after it is done.
- While one thread holds the mutex, other threads wait at lock instead of touching the data.
- The main thread reads shared data only after joining every thread that updates it, even when a mutex guards the updates.
- A mutex guards a whole update, so two values changed together stay consistent.
Three workers each add one deposit to a shared balance. Each worker locks the mutex, updates the balance and the update count together, and unlocks. The main thread prints both values only after joining all three workers.
Try it Yourself
Edit the code, then press Run.
Running is simulated
Exercise
When does a thread lock a mutex?