authorgravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2026-02-02 20:18:14-08:00
committergravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2026-02-02 20:18:14-08:00
log4c4e9d054e37afe82a856f5845c548f177996bd4
treed16099222c409fbe01cc1b0a3544119a04951532
parent550da1b676d059ae39a629d60da0f9cd155a5e89

std.Io: add RwLock and Semaphore sync primitives

and restore usage by std.debug.SelfInfo.Elf

5 files changed, 342 insertions(+), 20 deletions(-)

lib/std/Io.zig+14-1
......@@ -47,6 +47,9 @@ pub const Dir = @import("Io/Dir.zig");
4747pub const File = @import("Io/File.zig");
4848pub const Terminal = @import("Io/Terminal.zig");
4949
50pub const RwLock = @import("Io/RwLock.zig");
51pub const Semaphore = @import("Io/Semaphore.zig");
52
5053pub const VTable = struct {
5154 /// If it returns `null` it means `result` has been already populated and
5255 /// `await` will be a no-op.
......@@ -882,7 +885,7 @@ pub const Timeout = union(enum) {
882885
883886 pub const Error = error{ Timeout, UnsupportedClock };
884887
885 pub fn toDeadline(t: Timeout, io: Io) Clock.Error!?Clock.Timestamp {
888 pub fn toTimestamp(t: Timeout, io: Io) Clock.Error!?Clock.Timestamp {
886889 return switch (t) {
887890 .none => null,
888891 .duration => |d| try .fromNow(io, d),
......@@ -890,6 +893,14 @@ pub const Timeout = union(enum) {
890893 };
891894 }
892895
896 pub fn toDeadline(t: Timeout, io: Io) Timeout {
897 return switch (t) {
898 .none => .none,
899 .duration => |d| .{ .deadline = Clock.Timestamp.fromNow(io, d) catch @panic("TODO") },
900 .deadline => |d| .{ .deadline = d },
901 };
902 }
903
893904 pub fn toDurationFromNow(t: Timeout, io: Io) Clock.Error!?Clock.Duration {
894905 return switch (t) {
895906 .none => null,
......@@ -2153,5 +2164,7 @@ test {
21532164 _ = Writer;
21542165 _ = Evented;
21552166 _ = Threaded;
2167 _ = RwLock;
2168 _ = Semaphore;
21562169 _ = @import("Io/test.zig");
21572170}
lib/std/Io/RwLock.zig created+238
......@@ -0,0 +1,238 @@
1//! A lock that supports one writer or many readers.
2const RwLock = @This();
3
4const builtin = @import("builtin");
5
6const std = @import("../std.zig");
7const Io = std.Io;
8const assert = std.debug.assert;
9const testing = std.testing;
10
11state: usize,
12mutex: Io.Mutex,
13semaphore: Io.Semaphore,
14
15pub const init: RwLock = .{
16 .state = 0,
17 .mutex = .init,
18 .semaphore = .{},
19};
20
21const is_writing: usize = 1;
22const writer: usize = 1 << 1;
23const reader: usize = 1 << (1 + @bitSizeOf(Count));
24const writer_mask: usize = std.math.maxInt(Count) << @ctz(writer);
25const reader_mask: usize = std.math.maxInt(Count) << @ctz(reader);
26const Count = @Int(.unsigned, @divFloor(@bitSizeOf(usize) - 1, 2));
27
28pub fn tryLock(rl: *RwLock, io: Io) bool {
29 if (rl.mutex.tryLock()) {
30 const state = @atomicLoad(usize, &rl.state, .seq_cst);
31 if (state & reader_mask == 0) {
32 _ = @atomicRmw(usize, &rl.state, .Or, is_writing, .seq_cst);
33 return true;
34 }
35
36 rl.mutex.unlock(io);
37 }
38
39 return false;
40}
41
42pub fn lockUncancelable(rl: *RwLock, io: Io) void {
43 _ = @atomicRmw(usize, &rl.state, .Add, writer, .seq_cst);
44 rl.mutex.lockUncancelable(io);
45
46 const state = @atomicRmw(usize, &rl.state, .Add, is_writing -% writer, .seq_cst);
47 if (state & reader_mask != 0)
48 rl.semaphore.waitUncancelable(io);
49}
50
51pub fn unlock(rl: *RwLock, io: Io) void {
52 _ = @atomicRmw(usize, &rl.state, .And, ~is_writing, .seq_cst);
53 rl.mutex.unlock(io);
54}
55
56pub fn tryLockShared(rl: *RwLock, io: Io) bool {
57 const state = @atomicLoad(usize, &rl.state, .seq_cst);
58 if (state & (is_writing | writer_mask) == 0) {
59 _ = @cmpxchgStrong(
60 usize,
61 &rl.state,
62 state,
63 state + reader,
64 .seq_cst,
65 .seq_cst,
66 ) orelse return true;
67 }
68
69 if (rl.mutex.tryLock()) {
70 _ = @atomicRmw(usize, &rl.state, .Add, reader, .seq_cst);
71 rl.mutex.unlock(io);
72 return true;
73 }
74
75 return false;
76}
77
78pub fn lockSharedUncancelable(rl: *RwLock, io: Io) void {
79 var state = @atomicLoad(usize, &rl.state, .seq_cst);
80 while (state & (is_writing | writer_mask) == 0) {
81 state = @cmpxchgWeak(
82 usize,
83 &rl.state,
84 state,
85 state + reader,
86 .seq_cst,
87 .seq_cst,
88 ) orelse return;
89 }
90
91 rl.mutex.lockUncancelable(io);
92 _ = @atomicRmw(usize, &rl.state, .Add, reader, .seq_cst);
93 rl.mutex.unlock(io);
94}
95
96pub fn unlockShared(rl: *RwLock, io: Io) void {
97 const state = @atomicRmw(usize, &rl.state, .Sub, reader, .seq_cst);
98
99 if ((state & reader_mask == reader) and (state & is_writing != 0))
100 rl.semaphore.post(io);
101}
102
103test "internal state" {
104 const io = testing.io;
105
106 var rl: Io.RwLock = .init;
107
108 // The following failed prior to the fix for Issue #13163,
109 // where the WRITER flag was subtracted by the lock method.
110
111 rl.lockUncancelable(io);
112 rl.unlock(io);
113 try testing.expectEqual(rl, Io.RwLock.init);
114}
115
116test "smoke test" {
117 const io = testing.io;
118
119 var rl: Io.RwLock = .init;
120
121 rl.lockUncancelable(io);
122 try testing.expect(!rl.tryLock(io));
123 try testing.expect(!rl.tryLockShared(io));
124 rl.unlock(io);
125
126 try testing.expect(rl.tryLock(io));
127 try testing.expect(!rl.tryLock(io));
128 try testing.expect(!rl.tryLockShared(io));
129 rl.unlock(io);
130
131 rl.lockSharedUncancelable(io);
132 try testing.expect(!rl.tryLock(io));
133 try testing.expect(rl.tryLockShared(io));
134 rl.unlockShared(io);
135 rl.unlockShared(io);
136
137 try testing.expect(rl.tryLockShared(io));
138 try testing.expect(!rl.tryLock(io));
139 try testing.expect(rl.tryLockShared(io));
140 rl.unlockShared(io);
141 rl.unlockShared(io);
142
143 rl.lockUncancelable(io);
144 rl.unlock(io);
145}
146
147test "concurrent access" {
148 if (builtin.single_threaded) return;
149
150 const io = testing.io;
151 const num_writers: usize = 2;
152 const num_readers: usize = 4;
153 const num_writes: usize = 1000;
154 const num_reads: usize = 2000;
155
156 const Runner = struct {
157 const Runner = @This();
158
159 io: Io,
160
161 rl: Io.RwLock,
162 writes: usize,
163 reads: std.atomic.Value(usize),
164
165 val_a: usize,
166 val_b: usize,
167
168 fn reader(run: *Runner, thread_idx: usize) !void {
169 var prng = std.Random.DefaultPrng.init(thread_idx);
170 const rnd = prng.random();
171 while (true) {
172 run.rl.lockSharedUncancelable(run.io);
173 defer run.rl.unlockShared(run.io);
174
175 try testing.expect(run.writes <= num_writes);
176 if (run.reads.fetchAdd(1, .monotonic) >= num_reads) break;
177
178 // We use `volatile` accesses so that we can make sure the memory is accessed either
179 // side of a yield, maximising chances of a race.
180 const a_ptr: *const volatile usize = &run.val_a;
181 const b_ptr: *const volatile usize = &run.val_b;
182
183 const old_a = a_ptr.*;
184 if (rnd.boolean()) try std.Thread.yield();
185 const old_b = b_ptr.*;
186 try testing.expect(old_a == old_b);
187 }
188 }
189
190 fn writer(run: *Runner, thread_idx: usize) !void {
191 var prng = std.Random.DefaultPrng.init(thread_idx);
192 const rnd = prng.random();
193 while (true) {
194 run.rl.lockUncancelable(run.io);
195 defer run.rl.unlock(run.io);
196
197 try testing.expect(run.writes <= num_writes);
198 if (run.writes == num_writes) break;
199
200 // We use `volatile` accesses so that we can make sure the memory is accessed either
201 // side of a yield, maximising chances of a race.
202 const a_ptr: *volatile usize = &run.val_a;
203 const b_ptr: *volatile usize = &run.val_b;
204
205 const new_val = rnd.int(usize);
206
207 const old_a = a_ptr.*;
208 a_ptr.* = new_val;
209 if (rnd.boolean()) try std.Thread.yield();
210 const old_b = b_ptr.*;
211 b_ptr.* = new_val;
212 try testing.expect(old_a == old_b);
213
214 run.writes += 1;
215 }
216 }
217 };
218
219 var run: Runner = .{
220 .io = io,
221 .rl = .init,
222 .writes = 0,
223 .reads = .init(0),
224 .val_a = 0,
225 .val_b = 0,
226 };
227 var write_threads: [num_writers]std.Thread = undefined;
228 var read_threads: [num_readers]std.Thread = undefined;
229
230 for (&write_threads, 0..) |*t, i| t.* = try .spawn(.{}, Runner.writer, .{ &run, i });
231 for (&read_threads, num_writers..) |*t, i| t.* = try .spawn(.{}, Runner.reader, .{ &run, i });
232
233 for (write_threads) |t| t.join();
234 for (read_threads) |t| t.join();
235
236 try testing.expect(run.writes == num_writes);
237 try testing.expect(run.reads.raw >= num_reads);
238}
lib/std/Io/Semaphore.zig created+65
......@@ -0,0 +1,65 @@
1//! An unsigned integer that blocks the kernel thread if the number would
2//! become negative.
3//!
4//! This API supports static initialization and does not require deinitialization.
5const Semaphore = @This();
6
7const builtin = @import("builtin");
8
9const std = @import("../std.zig");
10const Io = std.Io;
11const testing = std.testing;
12
13mutex: Io.Mutex = .init,
14cond: Io.Condition = .init,
15/// It is OK to initialize this field to any value.
16permits: usize = 0,
17
18pub fn wait(s: *Semaphore, io: Io) Io.Cancelable!void {
19 try s.mutex.lock(io);
20 defer s.mutex.unlock(io);
21 while (s.permits == 0) try s.cond.wait(io, &s.mutex);
22 s.permits -= 1;
23 if (s.permits > 0) s.cond.signal(io);
24}
25
26pub fn waitUncancelable(s: *Semaphore, io: Io) void {
27 s.mutex.lockUncancelable(io);
28 defer s.mutex.unlock(io);
29 while (s.permits == 0) s.cond.waitUncancelable(io, &s.mutex);
30 s.permits -= 1;
31 if (s.permits > 0) s.cond.signal(io);
32}
33
34pub fn post(s: *Semaphore, io: Io) void {
35 s.mutex.lockUncancelable(io);
36 defer s.mutex.unlock(io);
37
38 s.permits += 1;
39 s.cond.signal(io);
40}
41
42test Semaphore {
43 if (builtin.single_threaded) return error.SkipZigTest;
44 const io = testing.io;
45
46 const TestContext = struct {
47 sem: *Semaphore,
48 n: *i32,
49 fn worker(ctx: *@This()) !void {
50 try ctx.sem.wait(io);
51 ctx.n.* += 1;
52 ctx.sem.post(io);
53 }
54 };
55 const num_threads = 3;
56 var sem: Semaphore = .{ .permits = 1 };
57 var threads: [num_threads]std.Thread = undefined;
58 var n: i32 = 0;
59 var ctx = TestContext{ .sem = &sem, .n = &n };
60
61 for (&threads) |*t| t.* = try std.Thread.spawn(.{}, TestContext.worker, .{&ctx});
62 for (threads) |t| t.join();
63 try sem.wait(io);
64 try testing.expect(n == num_threads);
65}
lib/std/Io/Threaded.zig+4-4
......@@ -2655,7 +2655,7 @@ fn batchAwaitAsync(userdata: ?*anyopaque, b: *Io.Batch) Io.Cancelable!void {
26552655fn batchAwaitConcurrent(userdata: ?*anyopaque, b: *Io.Batch, timeout: Io.Timeout) Io.Batch.AwaitConcurrentError!void {
26562656 const t: *Threaded = @ptrCast(@alignCast(userdata));
26572657 if (is_windows) {
2658 const deadline: ?Io.Clock.Timestamp = timeout.toDeadline(ioBasic(t)) catch |err| switch (err) {
2658 const deadline: ?Io.Clock.Timestamp = timeout.toTimestamp(ioBasic(t)) catch |err| switch (err) {
26592659 error.Unexpected => deadline: {
26602660 recoverableOsBugDetected();
26612661 break :deadline .{ .raw = .{ .nanoseconds = 0 }, .clock = .awake };
......@@ -2754,7 +2754,7 @@ fn batchAwaitConcurrent(userdata: ?*anyopaque, b: *Io.Batch, timeout: Io.Timeout
27542754 else => {},
27552755 }
27562756 const t_io = ioBasic(t);
2757 const deadline = timeout.toDeadline(t_io) catch return error.UnsupportedClock;
2757 const deadline = timeout.toTimestamp(t_io) catch return error.UnsupportedClock;
27582758 while (true) {
27592759 const timeout_ms: i32 = t: {
27602760 if (b.completions.head != .none) {
......@@ -10918,7 +10918,7 @@ fn nowWasi(clock: Io.Clock) Io.Clock.Error!Io.Timestamp {
1091810918fn sleep(userdata: ?*anyopaque, timeout: Io.Timeout) Io.SleepError!void {
1091910919 const t: *Threaded = @ptrCast(@alignCast(userdata));
1092010920 if (timeout == .none) return;
10921 if (use_parking_sleep) return parking_sleep.sleep(try timeout.toDeadline(ioBasic(t)));
10921 if (use_parking_sleep) return parking_sleep.sleep(try timeout.toTimestamp(ioBasic(t)));
1092210922 if (native_os == .wasi) return sleepWasi(t, timeout);
1092310923 if (@TypeOf(posix.system.clock_nanosleep) != void) return sleepPosix(timeout);
1092410924 return sleepNanosleep(t, timeout);
......@@ -12630,7 +12630,7 @@ fn netReceivePosix(
1263012630 var message_i: usize = 0;
1263112631 var data_i: usize = 0;
1263212632
12633 const deadline = timeout.toDeadline(t_io) catch |err| return .{ err, message_i };
12633 const deadline = timeout.toTimestamp(t_io) catch |err| return .{ err, message_i };
1263412634
1263512635 recv: while (true) {
1263612636 if (message_buffer.len - message_i == 0) return .{ null, message_i };
lib/std/debug/SelfInfo/Elf.zig+21-15
......@@ -1,4 +1,4 @@
1mutex: Io.Mutex,
1rwlock: Io.RwLock,
22
33modules: std.ArrayList(Module),
44ranges: std.ArrayList(Module.Range),
......@@ -6,7 +6,7 @@ ranges: std.ArrayList(Module.Range),
66unwind_cache: if (can_unwind) ?[]Dwarf.SelfUnwinder.CacheEntry else ?noreturn,
77
88pub const init: SelfInfo = .{
9 .mutex = .init,
9 .rwlock = .init,
1010 .modules = .empty,
1111 .ranges = .empty,
1212 .unwind_cache = null,
......@@ -30,7 +30,7 @@ pub fn deinit(si: *SelfInfo, gpa: Allocator) void {
3030
3131pub fn getSymbol(si: *SelfInfo, gpa: Allocator, io: Io, address: usize) Error!std.debug.Symbol {
3232 const module = try si.findModule(gpa, io, address, .exclusive);
33 defer si.mutex.unlock(io);
33 defer si.rwlock.unlock(io);
3434
3535 const vaddr = address - module.load_offset;
3636
......@@ -75,13 +75,13 @@ pub fn getSymbol(si: *SelfInfo, gpa: Allocator, io: Io, address: usize) Error!st
7575}
7676pub fn getModuleName(si: *SelfInfo, gpa: Allocator, io: Io, address: usize) Error![]const u8 {
7777 const module = try si.findModule(gpa, io, address, .shared);
78 defer si.mutex.unlock(io);
78 defer si.rwlock.unlockShared(io);
7979 if (module.name.len == 0) return error.MissingDebugInfo;
8080 return module.name;
8181}
8282pub fn getModuleSlide(si: *SelfInfo, gpa: Allocator, io: Io, address: usize) Error!usize {
8383 const module = try si.findModule(gpa, io, address, .shared);
84 defer si.mutex.unlock(io);
84 defer si.rwlock.unlockShared(io);
8585 return module.load_offset;
8686}
8787
......@@ -183,8 +183,8 @@ pub fn unwindFrame(si: *SelfInfo, gpa: Allocator, io: Io, context: *UnwindContex
183183 comptime assert(can_unwind);
184184
185185 {
186 try si.mutex.lock(io);
187 defer si.mutex.unlock(io);
186 si.rwlock.lockSharedUncancelable(io);
187 defer si.rwlock.unlockShared(io);
188188 if (si.unwind_cache) |cache| {
189189 if (Dwarf.SelfUnwinder.CacheEntry.find(cache, context.pc)) |entry| {
190190 return context.next(gpa, entry);
......@@ -193,7 +193,7 @@ pub fn unwindFrame(si: *SelfInfo, gpa: Allocator, io: Io, context: *UnwindContex
193193 }
194194
195195 const module = try si.findModule(gpa, io, context.pc, .exclusive);
196 defer si.mutex.unlock(io);
196 defer si.rwlock.unlock(io);
197197
198198 if (si.unwind_cache == null) {
199199 si.unwind_cache = try gpa.alloc(Dwarf.SelfUnwinder.CacheEntry, 2048);
......@@ -378,8 +378,8 @@ const Module = struct {
378378fn findModule(si: *SelfInfo, gpa: Allocator, io: Io, address: usize, lock: enum { shared, exclusive }) Error!*Module {
379379 // With the requested lock, scan the module ranges looking for `address`.
380380 switch (lock) {
381 .shared => try si.mutex.lock(io),
382 .exclusive => try si.mutex.lock(io),
381 .shared => si.rwlock.lockSharedUncancelable(io),
382 .exclusive => si.rwlock.lockUncancelable(io),
383383 }
384384 for (si.ranges.items) |*range| {
385385 if (address >= range.start and address < range.start + range.len) {
......@@ -389,12 +389,15 @@ fn findModule(si: *SelfInfo, gpa: Allocator, io: Io, address: usize, lock: enum
389389 // The address wasn't in a known range. We will rebuild the module/range lists, since it's possible
390390 // a new module was loaded. Upgrade to an exclusive lock if necessary.
391391 switch (lock) {
392 .shared => {},
392 .shared => {
393 si.rwlock.unlockShared(io);
394 si.rwlock.lockUncancelable(io);
395 },
393396 .exclusive => {},
394397 }
395398 // Rebuild module list with the exclusive lock.
396399 {
397 errdefer si.mutex.unlock(io);
400 errdefer si.rwlock.unlock(io);
398401 for (si.modules.items) |*mod| {
399402 unwind: {
400403 const u = &(mod.unwind orelse break :unwind catch break :unwind);
......@@ -412,7 +415,10 @@ fn findModule(si: *SelfInfo, gpa: Allocator, io: Io, address: usize, lock: enum
412415 }
413416 // Downgrade the lock back to shared if necessary.
414417 switch (lock) {
415 .shared => {},
418 .shared => {
419 si.rwlock.unlock(io);
420 si.rwlock.lockSharedUncancelable(io);
421 },
416422 .exclusive => {},
417423 }
418424 // Scan the newly rebuilt module ranges.
......@@ -423,8 +429,8 @@ fn findModule(si: *SelfInfo, gpa: Allocator, io: Io, address: usize, lock: enum
423429 }
424430 // Still nothing; unlock and error.
425431 switch (lock) {
426 .shared => si.mutex.unlock(io),
427 .exclusive => si.mutex.unlock(io),
432 .shared => si.rwlock.unlockShared(io),
433 .exclusive => si.rwlock.unlock(io),
428434 }
429435 return error.MissingDebugInfo;
430436}