authorgravatar for jay@jayschwa.netJay Petacat <jay@jayschwa.net> 2026-09-04 22:37:27-06:00
committergravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2026-09-09 02:18:52+02:00
logd7148f7d056a5891a2b4c8e22ce71ef09d60d356
tree2ed6300eb2cf2cbd7677a5f1f54c9bfffa4b9c30
parent85e44dbae2af3ce6d71210f6d91014ca09b9597a

std.debug.SafetyLock: add support for shared locking

The existing `lock` and `unlock` methods continue to act exclusively. They should be used when data may be mutated. New methods, `lockShared` and `unlockShared`, may be used for shared locking in situations where multiple independent users are reading but not mutating data.

4 files changed, 74 insertions(+), 32 deletions(-)

lib/std/array_hash_map.zig+7-6
...@@ -200,17 +200,18 @@ pub fn Custom(...@@ -200,17 +200,18 @@ pub fn Custom(
200 /// cause an existing key or value pointer to become invalidated will200 /// cause an existing key or value pointer to become invalidated will
201 /// instead trigger an assertion.201 /// instead trigger an assertion.
202 ///202 ///
203 /// An additional call to `lockPointers` in such state also triggers an203 /// `lockPointers` may be called multiple times. This allows multiple
204 /// assertion.204 /// independent users of the hash map to keep it locked simultaneously.
205 ///205 ///
206 /// `unlockPointers` returns the hash map to the previous state.206 /// `unlockPointers` restores the hash map to its previous state when
207 /// called the same number of times as `lockPointers`.
207 pub fn lockPointers(self: *Self) void {208 pub fn lockPointers(self: *Self) void {
208 self.pointer_stability.lock();209 self.pointer_stability.lockShared();
209 }210 }
210211
211 /// Undoes a call to `lockPointers`.212 /// Undoes one call to `lockPointers`.
212 pub fn unlockPointers(self: *Self) void {213 pub fn unlockPointers(self: *Self) void {
213 self.pointer_stability.unlock();214 self.pointer_stability.unlockShared();
214 }215 }
215216
216 /// Clears the map but retains the backing allocation for future use.217 /// Clears the map but retains the backing allocation for future use.
lib/std/array_list.zig+14-12
...@@ -74,17 +74,18 @@ pub fn AlignedManaged(comptime T: type, comptime alignment: ?mem.Alignment) type...@@ -74,17 +74,18 @@ pub fn AlignedManaged(comptime T: type, comptime alignment: ?mem.Alignment) type
74 /// cause an existing value pointer to become invalidated will74 /// cause an existing value pointer to become invalidated will
75 /// instead trigger an assertion.75 /// instead trigger an assertion.
76 ///76 ///
77 /// An additional call to `lockPointers` in such state also triggers an77 /// `lockPointers` may be called multiple times. This allows multiple
78 /// assertion.78 /// independent users of the list to keep it locked simultaneously.
79 ///79 ///
80 /// `unlockPointers` returns the array list to the previous state.80 /// `unlockPointers` restores the list to its previous state when
81 /// called the same number of times as `lockPointers`.
81 pub fn lockPointers(self: *Self) void {82 pub fn lockPointers(self: *Self) void {
82 self.pointer_stability.lock();83 self.pointer_stability.lockShared();
83 }84 }
8485
85 /// Undoes a call to `lockPointers`.86 /// Undoes one call to `lockPointers`.
86 pub fn unlockPointers(self: *Self) void {87 pub fn unlockPointers(self: *Self) void {
87 self.pointer_stability.unlock();88 self.pointer_stability.unlockShared();
88 }89 }
8990
90 /// ArrayList takes ownership of the passed in slice. The slice must have been91 /// ArrayList takes ownership of the passed in slice. The slice must have been
...@@ -698,17 +699,18 @@ pub fn Aligned(comptime T: type, comptime alignment: ?mem.Alignment) type {...@@ -698,17 +699,18 @@ pub fn Aligned(comptime T: type, comptime alignment: ?mem.Alignment) type {
698 /// cause an existing value pointer to become invalidated will699 /// cause an existing value pointer to become invalidated will
699 /// instead trigger an assertion.700 /// instead trigger an assertion.
700 ///701 ///
701 /// An additional call to `lockPointers` in such state also triggers an702 /// `lockPointers` may be called multiple times. This allows multiple
702 /// assertion.703 /// independent users of the list to keep it locked simultaneously.
703 ///704 ///
704 /// `unlockPointers` returns the unmanaged array list to the previous state.705 /// `unlockPointers` restores the list to its previous state when
706 /// called the same number of times as `lockPointers`.
705 pub fn lockPointers(self: *Self) void {707 pub fn lockPointers(self: *Self) void {
706 self.pointer_stability.lock();708 self.pointer_stability.lockShared();
707 }709 }
708710
709 /// Undoes a call to `lockPointers`.711 /// Undoes one call to `lockPointers`.
710 pub fn unlockPointers(self: *Self) void {712 pub fn unlockPointers(self: *Self) void {
711 self.pointer_stability.unlock();713 self.pointer_stability.unlockShared();
712 }714 }
713715
714 /// Convert this list into an analogous memory-managed one.716 /// Convert this list into an analogous memory-managed one.
lib/std/debug.zig+41-4
...@@ -1851,20 +1851,46 @@ pub fn ConfigurableTrace(comptime size: usize, comptime stack_frame_count: usize...@@ -1851,20 +1851,46 @@ pub fn ConfigurableTrace(comptime size: usize, comptime stack_frame_count: usize
1851pub const SafetyLock = struct {1851pub const SafetyLock = struct {
1852 state: State = if (runtime_safety) .unlocked else .unknown,1852 state: State = if (runtime_safety) .unlocked else .unknown,
18531853
1854 pub const State = if (runtime_safety) enum { unlocked, locked } else enum { unknown };1854 pub const State = if (runtime_safety) enum(usize) {
1855 unlocked = 0,
1856 exclusive = math.maxInt(usize),
1857 _, // shared lock count
1858
1859 fn isShared(state: State) bool {
1860 return switch (state) {
1861 _ => true,
1862 else => false,
1863 };
1864 }
1865 } else enum { unknown };
18551866
1867 /// Exclusive. Use when mutating data.
1856 pub fn lock(l: *SafetyLock) void {1868 pub fn lock(l: *SafetyLock) void {
1857 if (!runtime_safety) return;1869 if (!runtime_safety) return;
1858 assert(l.state == .unlocked);1870 assert(l.state == .unlocked);
1859 l.state = .locked;1871 l.state = .exclusive;
1860 }1872 }
18611873
1862 pub fn unlock(l: *SafetyLock) void {1874 pub fn unlock(l: *SafetyLock) void {
1863 if (!runtime_safety) return;1875 if (!runtime_safety) return;
1864 assert(l.state == .locked);1876 assert(l.state == .exclusive);
1865 l.state = .unlocked;1877 l.state = .unlocked;
1866 }1878 }
18671879
1880 /// Use when consuming data in a read-only manner.
1881 pub fn lockShared(l: *SafetyLock) void {
1882 if (!runtime_safety) return;
1883 assert(l.state != .exclusive);
1884 l.state = @fromBackingInt(@backingInt(l.state) + 1);
1885 assert(l.state.isShared()); // Catch overflow to `exclusive`.
1886 }
1887
1888 pub fn unlockShared(l: *SafetyLock) void {
1889 if (!runtime_safety) return;
1890 assert(l.state.isShared());
1891 l.state = @fromBackingInt(@backingInt(l.state) - 1);
1892 }
1893
1868 pub fn assertUnlocked(l: SafetyLock) void {1894 pub fn assertUnlocked(l: SafetyLock) void {
1869 if (!runtime_safety) return;1895 if (!runtime_safety) return;
1870 assert(l.state == .unlocked);1896 assert(l.state == .unlocked);
...@@ -1872,7 +1898,12 @@ pub const SafetyLock = struct {...@@ -1872,7 +1898,12 @@ pub const SafetyLock = struct {
18721898
1873 pub fn assertLocked(l: SafetyLock) void {1899 pub fn assertLocked(l: SafetyLock) void {
1874 if (!runtime_safety) return;1900 if (!runtime_safety) return;
1875 assert(l.state == .locked);1901 assert(l.state == .exclusive);
1902 }
1903
1904 pub fn assertLockedShared(l: SafetyLock) void {
1905 if (!runtime_safety) return;
1906 assert(l.state.isShared());
1876 }1907 }
1877};1908};
18781909
...@@ -1883,6 +1914,12 @@ test SafetyLock {...@@ -1883,6 +1914,12 @@ test SafetyLock {
1883 safety_lock.assertLocked();1914 safety_lock.assertLocked();
1884 safety_lock.unlock();1915 safety_lock.unlock();
1885 safety_lock.assertUnlocked();1916 safety_lock.assertUnlocked();
1917 safety_lock.lockShared();
1918 safety_lock.assertLockedShared();
1919 for (0..3) |_| safety_lock.lockShared();
1920 safety_lock.assertLockedShared();
1921 for (0..4) |_| safety_lock.unlockShared();
1922 safety_lock.assertUnlocked();
1886}1923}
18871924
1888/// Detect whether the program is being executed in the Valgrind virtual machine.1925/// Detect whether the program is being executed in the Valgrind virtual machine.
lib/std/hash_map.zig+12-10
...@@ -191,15 +191,16 @@ pub fn HashMap(...@@ -191,15 +191,16 @@ pub fn HashMap(
191 /// cause an existing key or value pointer to become invalidated will191 /// cause an existing key or value pointer to become invalidated will
192 /// instead trigger an assertion.192 /// instead trigger an assertion.
193 ///193 ///
194 /// An additional call to `lockPointers` in such state also triggers an194 /// `lockPointers` may be called multiple times. This allows multiple
195 /// assertion.195 /// independent users of the hash map to keep it locked simultaneously.
196 ///196 ///
197 /// `unlockPointers` returns the hash map to the previous state.197 /// `unlockPointers` restores the hash map to its previous state when
198 /// called the same number of times as `lockPointers`.
198 pub fn lockPointers(self: *Self) void {199 pub fn lockPointers(self: *Self) void {
199 self.unmanaged.lockPointers();200 self.unmanaged.lockPointers();
200 }201 }
201202
202 /// Undoes a call to `lockPointers`.203 /// Undoes one call to `lockPointers`.
203 pub fn unlockPointers(self: *Self) void {204 pub fn unlockPointers(self: *Self) void {
204 self.unmanaged.unlockPointers();205 self.unmanaged.unlockPointers();
205 }206 }
...@@ -709,17 +710,18 @@ fn Custom(...@@ -709,17 +710,18 @@ fn Custom(
709 /// cause an existing key or value pointer to become invalidated will710 /// cause an existing key or value pointer to become invalidated will
710 /// instead trigger an assertion.711 /// instead trigger an assertion.
711 ///712 ///
712 /// An additional call to `lockPointers` in such state also triggers an713 /// `lockPointers` may be called multiple times. This allows multiple
713 /// assertion.714 /// independent users of the hash map to keep it locked simultaneously.
714 ///715 ///
715 /// `unlockPointers` returns the hash map to the previous state.716 /// `unlockPointers` restores the hash map to its previous state when
717 /// called the same number of times as `lockPointers`.
716 pub fn lockPointers(self: *Self) void {718 pub fn lockPointers(self: *Self) void {
717 self.pointer_stability.lock();719 self.pointer_stability.lockShared();
718 }720 }
719721
720 /// Undoes a call to `lockPointers`.722 /// Undoes one call to `lockPointers`.
721 pub fn unlockPointers(self: *Self) void {723 pub fn unlockPointers(self: *Self) void {
722 self.pointer_stability.unlock();724 self.pointer_stability.unlockShared();
723 }725 }
724726
725 fn isUnderMaxLoadPercentage(size: Size, cap: Size) bool {727 fn isUnderMaxLoadPercentage(size: Size, cap: Size) bool {