Zig Atomics
An atomic value is a value that threads can update safely without taking turns. Several threads can add to one atomic counter, and the final total is exact.
Two threads, one counter
Two workers each add 10 to one atomic counter. The main thread joins both workers, then loads and prints the total.
const std = @import("std");
fn addTen(counter: *std.atomic.Value(i32)) void {
_ = counter.fetchAdd(10, .seq_cst);
}
pub fn main() !void {
var out = std.io.getStdOut().writer();
var counter = std.atomic.Value(i32).init(0);
var t1 = try std.Thread.spawn(.{}, addTen, .{&counter});
var t2 = try std.Thread.spawn(.{}, addTen, .{&counter});
t1.join();
t2.join();
try out.print("count={d}\n", .{counter.load(.seq_cst)});
}One thread shows the old value
fetchAdd returns the value held before the add. With a single worker, that old value is certain, so the main thread can print it.
const std = @import("std");
fn addOld(counter: *std.atomic.Value(i32), slot: *i32) void {
slot.* = counter.fetchAdd(7, .seq_cst);
}
pub fn main() !void {
var out = std.io.getStdOut().writer();
var counter = std.atomic.Value(i32).init(10);
var old: i32 = 0;
var t = try std.Thread.spawn(.{}, addOld, .{ &counter, &old });
t.join();
try out.print("old={d} final={d}\n", .{ old, counter.load(.seq_cst) });
}Four threads add at once
Four workers each add 25 to the same atomic counter. No worker waits for another, and the joined total is exact.
const std = @import("std");
fn addAmount(counter: *std.atomic.Value(i32), amount: i32) void {
_ = counter.fetchAdd(amount, .seq_cst);
}
pub fn main() !void {
var out = std.io.getStdOut().writer();
var counter = std.atomic.Value(i32).init(0);
var threads: [4]std.Thread = undefined;
for (0..4) |i| {
threads[i] = try std.Thread.spawn(.{}, addAmount, .{ &counter, 25 });
}
for (threads) |t| {
t.join();
}
try out.print("count={d}\n", .{counter.load(.seq_cst)});
}Example
const std = @import("std");
fn addAmount(counter: *std.atomic.Value(i32), amount: i32) void {
_ = counter.fetchAdd(amount, .seq_cst);
}
pub fn main() !void {
var out = std.io.getStdOut().writer();
var counter = std.atomic.Value(i32).init(0);
const amounts: [3]i32 = .{ 5, 10, 15 };
var threads: [3]std.Thread = undefined;
for (0..3) |i| {
threads[i] = try std.Thread.spawn(.{}, addAmount, .{ &counter, amounts[i] });
}
for (threads) |t| {
t.join();
}
try out.print("total={d}\n", .{counter.load(.seq_cst)});
}Key points
- Atomic: an atomic value is a value that threads can update safely at the same time.
- std.atomic.Value holds one atomic value of a given integer type.
- Ordering: an ordering is a rule about how threads observe each other's updates.
- The .seq_cst ordering is the strictest ordering, and this course uses it for every atomic operation.
- fetchAdd adds to an atomic counter and returns the value it held before the add.
- load reads the current value of an atomic value.
- The main thread loads an atomic counter only after joining every thread that updates it.
Three workers add different amounts to one atomic counter. Each add is atomic, so no amount is lost even when workers run at the same time. The main thread loads the total only after joining all three workers.
Try it Yourself
Edit the code, then press Run.
Running is simulated
Exercise
What does fetchAdd do to an atomic counter?