1const builtin = @import("builtin");
2
3const std = @import("std.zig");
4const AtomicOrder = std.builtin.AtomicOrder;
5const testing = std.testing;
6const assert = std.debug.assert;
7
8/// This is a thin wrapper around a primitive value to prevent accidental data races.
9pub fn Value(comptime T: type) type {
10 return extern struct {
11 /// Care must be taken to avoid data races when interacting with this field directly.
12 raw: T,
13
14 const Self = @This();
15
16 pub fn init(value: T) Self {
17 return .{ .raw = value };
18 }
19
20 pub inline fn load(self: *const Self, comptime order: AtomicOrder) T {
21 return @atomicLoad(T, &self.raw, order);
22 }
23
24 pub inline fn store(self: *Self, value: T, comptime order: AtomicOrder) void {
25 @atomicStore(T, &self.raw, value, order);
26 }
27
28 pub inline fn swap(self: *Self, operand: T, comptime order: AtomicOrder) T {
29 return @atomicRmw(T, &self.raw, .Xchg, operand, order);
30 }
31
32 pub inline fn cmpxchgWeak(
33 self: *Self,
34 expected_value: T,
35 new_value: T,
36 comptime success_order: AtomicOrder,
37 comptime fail_order: AtomicOrder,
38 ) ?T {
39 return @cmpxchgWeak(T, &self.raw, expected_value, new_value, success_order, fail_order);
40 }
41
42 pub inline fn cmpxchgStrong(
43 self: *Self,
44 expected_value: T,
45 new_value: T,
46 comptime success_order: AtomicOrder,
47 comptime fail_order: AtomicOrder,
48 ) ?T {
49 return @cmpxchgStrong(T, &self.raw, expected_value, new_value, success_order, fail_order);
50 }
51
52 pub inline fn fetchAdd(self: *Self, operand: T, comptime order: AtomicOrder) T {
53 return @atomicRmw(T, &self.raw, .Add, operand, order);
54 }
55
56 pub inline fn fetchSub(self: *Self, operand: T, comptime order: AtomicOrder) T {
57 return @atomicRmw(T, &self.raw, .Sub, operand, order);
58 }
59
60 pub inline fn fetchMin(self: *Self, operand: T, comptime order: AtomicOrder) T {
61 return @atomicRmw(T, &self.raw, .Min, operand, order);
62 }
63
64 pub inline fn fetchMax(self: *Self, operand: T, comptime order: AtomicOrder) T {
65 return @atomicRmw(T, &self.raw, .Max, operand, order);
66 }
67
68 pub inline fn fetchAnd(self: *Self, operand: T, comptime order: AtomicOrder) T {
69 return @atomicRmw(T, &self.raw, .And, operand, order);
70 }
71
72 pub inline fn fetchNand(self: *Self, operand: T, comptime order: AtomicOrder) T {
73 return @atomicRmw(T, &self.raw, .Nand, operand, order);
74 }
75
76 pub inline fn fetchXor(self: *Self, operand: T, comptime order: AtomicOrder) T {
77 return @atomicRmw(T, &self.raw, .Xor, operand, order);
78 }
79
80 pub inline fn fetchOr(self: *Self, operand: T, comptime order: AtomicOrder) T {
81 return @atomicRmw(T, &self.raw, .Or, operand, order);
82 }
83
84 pub inline fn rmw(
85 self: *Self,
86 comptime op: std.builtin.AtomicRmwOp,
87 operand: T,
88 comptime order: AtomicOrder,
89 ) T {
90 return @atomicRmw(T, &self.raw, op, operand, order);
91 }
92
93 const Bit = std.math.Log2Int(T);
94
95 /// Marked `inline` so that if `bit` is comptime-known, the instruction
96 /// can be lowered to a more efficient machine code instruction if
97 /// possible.
98 pub inline fn bitSet(self: *Self, bit: Bit, comptime order: AtomicOrder) u1 {
99 const mask = @as(T, 1) << bit;
100 const value = self.fetchOr(mask, order);
101 return @intFromBool(value & mask != 0);
102 }
103
104 /// Marked `inline` so that if `bit` is comptime-known, the instruction
105 /// can be lowered to a more efficient machine code instruction if
106 /// possible.
107 pub inline fn bitReset(self: *Self, bit: Bit, comptime order: AtomicOrder) u1 {
108 const mask = @as(T, 1) << bit;
109 const value = self.fetchAnd(~mask, order);
110 return @intFromBool(value & mask != 0);
111 }
112
113 /// Marked `inline` so that if `bit` is comptime-known, the instruction
114 /// can be lowered to a more efficient machine code instruction if
115 /// possible.
116 pub inline fn bitToggle(self: *Self, bit: Bit, comptime order: AtomicOrder) u1 {
117 const mask = @as(T, 1) << bit;
118 const value = self.fetchXor(mask, order);
119 return @intFromBool(value & mask != 0);
120 }
121 };
122}
123
124test Value {
125 const RefCount = struct {
126 count: Value(usize),
127 dropFn: *const fn (*RefCount) void,
128
129 const RefCount = @This();
130
131 fn ref(rc: *RefCount) void {
132 // no synchronization necessary; just updating a counter.
133 _ = rc.count.fetchAdd(1, .monotonic);
134 }
135
136 fn unref(rc: *RefCount) void {
137 // release ensures code before unref() happens-before the
138 // count is decremented as dropFn could be called by then.
139 if (rc.count.fetchSub(1, .release) == 1) {
140 // seeing 1 in the counter means that other unref()s have happened,
141 // but it doesn't mean that uses before each unref() are visible.
142 // The load acquires the release-sequence created by previous unref()s
143 // in order to ensure visibility of uses before dropping.
144 _ = rc.count.load(.acquire);
145 (rc.dropFn)(rc);
146 }
147 }
148
149 fn noop(rc: *RefCount) void {
150 _ = rc;
151 }
152 };
153
154 var ref_count: RefCount = .{
155 .count = Value(usize).init(0),
156 .dropFn = RefCount.noop,
157 };
158 ref_count.ref();
159 ref_count.unref();
160}
161
162test "Value.swap" {
163 var x = Value(usize).init(5);
164 try testing.expectEqual(@as(usize, 5), x.swap(10, .seq_cst));
165 try testing.expectEqual(@as(usize, 10), x.load(.seq_cst));
166
167 const E = enum(usize) { a, b, c };
168 var y = Value(E).init(.c);
169 try testing.expectEqual(E.c, y.swap(.a, .seq_cst));
170 try testing.expectEqual(E.a, y.load(.seq_cst));
171
172 var z = Value(f32).init(5.0);
173 try testing.expectEqual(@as(f32, 5.0), z.swap(10.0, .seq_cst));
174 try testing.expectEqual(@as(f32, 10.0), z.load(.seq_cst));
175
176 var a = Value(bool).init(false);
177 try testing.expectEqual(false, a.swap(true, .seq_cst));
178 try testing.expectEqual(true, a.load(.seq_cst));
179
180 var b = Value(?*u8).init(null);
181 try testing.expectEqual(@as(?*u8, null), b.swap(@as(?*u8, @ptrFromInt(@alignOf(u8))), .seq_cst));
182 try testing.expectEqual(@as(?*u8, @ptrFromInt(@alignOf(u8))), b.load(.seq_cst));
183}
184
185test "Value.store" {
186 var x = Value(usize).init(5);
187 x.store(10, .seq_cst);
188 try testing.expectEqual(@as(usize, 10), x.load(.seq_cst));
189}
190
191test "Value.cmpxchgWeak" {
192 var x = Value(usize).init(0);
193
194 try testing.expectEqual(@as(?usize, 0), x.cmpxchgWeak(1, 0, .seq_cst, .seq_cst));
195 try testing.expectEqual(@as(usize, 0), x.load(.seq_cst));
196
197 while (x.cmpxchgWeak(0, 1, .seq_cst, .seq_cst)) |_| {}
198 try testing.expectEqual(@as(usize, 1), x.load(.seq_cst));
199
200 while (x.cmpxchgWeak(1, 0, .seq_cst, .seq_cst)) |_| {}
201 try testing.expectEqual(@as(usize, 0), x.load(.seq_cst));
202}
203
204test "Value.cmpxchgStrong" {
205 var x = Value(usize).init(0);
206 try testing.expectEqual(@as(?usize, 0), x.cmpxchgStrong(1, 0, .seq_cst, .seq_cst));
207 try testing.expectEqual(@as(usize, 0), x.load(.seq_cst));
208 try testing.expectEqual(@as(?usize, null), x.cmpxchgStrong(0, 1, .seq_cst, .seq_cst));
209 try testing.expectEqual(@as(usize, 1), x.load(.seq_cst));
210 try testing.expectEqual(@as(?usize, null), x.cmpxchgStrong(1, 0, .seq_cst, .seq_cst));
211 try testing.expectEqual(@as(usize, 0), x.load(.seq_cst));
212}
213
214test "Value.fetchAdd" {
215 var x = Value(usize).init(5);
216 try testing.expectEqual(@as(usize, 5), x.fetchAdd(5, .seq_cst));
217 try testing.expectEqual(@as(usize, 10), x.load(.seq_cst));
218 try testing.expectEqual(@as(usize, 10), x.fetchAdd(std.math.maxInt(usize), .seq_cst));
219 try testing.expectEqual(@as(usize, 9), x.load(.seq_cst));
220}
221
222test "Value.fetchSub" {
223 var x = Value(usize).init(5);
224 try testing.expectEqual(@as(usize, 5), x.fetchSub(5, .seq_cst));
225 try testing.expectEqual(@as(usize, 0), x.load(.seq_cst));
226 try testing.expectEqual(@as(usize, 0), x.fetchSub(1, .seq_cst));
227 try testing.expectEqual(@as(usize, std.math.maxInt(usize)), x.load(.seq_cst));
228}
229
230test "Value.fetchMin" {
231 var x = Value(usize).init(5);
232 try testing.expectEqual(@as(usize, 5), x.fetchMin(0, .seq_cst));
233 try testing.expectEqual(@as(usize, 0), x.load(.seq_cst));
234 try testing.expectEqual(@as(usize, 0), x.fetchMin(10, .seq_cst));
235 try testing.expectEqual(@as(usize, 0), x.load(.seq_cst));
236}
237
238test "Value.fetchMax" {
239 var x = Value(usize).init(5);
240 try testing.expectEqual(@as(usize, 5), x.fetchMax(10, .seq_cst));
241 try testing.expectEqual(@as(usize, 10), x.load(.seq_cst));
242 try testing.expectEqual(@as(usize, 10), x.fetchMax(5, .seq_cst));
243 try testing.expectEqual(@as(usize, 10), x.load(.seq_cst));
244}
245
246test "Value.fetchAnd" {
247 var x = Value(usize).init(0b11);
248 try testing.expectEqual(@as(usize, 0b11), x.fetchAnd(0b10, .seq_cst));
249 try testing.expectEqual(@as(usize, 0b10), x.load(.seq_cst));
250 try testing.expectEqual(@as(usize, 0b10), x.fetchAnd(0b00, .seq_cst));
251 try testing.expectEqual(@as(usize, 0b00), x.load(.seq_cst));
252}
253
254test "Value.fetchNand" {
255 var x = Value(usize).init(0b11);
256 try testing.expectEqual(@as(usize, 0b11), x.fetchNand(0b10, .seq_cst));
257 try testing.expectEqual(~@as(usize, 0b10), x.load(.seq_cst));
258 try testing.expectEqual(~@as(usize, 0b10), x.fetchNand(0b00, .seq_cst));
259 try testing.expectEqual(~@as(usize, 0b00), x.load(.seq_cst));
260}
261
262test "Value.fetchOr" {
263 var x = Value(usize).init(0b11);
264 try testing.expectEqual(@as(usize, 0b11), x.fetchOr(0b100, .seq_cst));
265 try testing.expectEqual(@as(usize, 0b111), x.load(.seq_cst));
266 try testing.expectEqual(@as(usize, 0b111), x.fetchOr(0b010, .seq_cst));
267 try testing.expectEqual(@as(usize, 0b111), x.load(.seq_cst));
268}
269
270test "Value.fetchXor" {
271 var x = Value(usize).init(0b11);
272 try testing.expectEqual(@as(usize, 0b11), x.fetchXor(0b10, .seq_cst));
273 try testing.expectEqual(@as(usize, 0b01), x.load(.seq_cst));
274 try testing.expectEqual(@as(usize, 0b01), x.fetchXor(0b01, .seq_cst));
275 try testing.expectEqual(@as(usize, 0b00), x.load(.seq_cst));
276}
277
278test "Value.bitSet" {
279 var x = Value(usize).init(0);
280
281 for (0..@bitSizeOf(usize)) |bit_index| {
282 const bit = @as(std.math.Log2Int(usize), @intCast(bit_index));
283 const mask = @as(usize, 1) << bit;
284
285 // setting the bit should change the bit
286 try testing.expect(x.load(.seq_cst) & mask == 0);
287 try testing.expectEqual(@as(u1, 0), x.bitSet(bit, .seq_cst));
288 try testing.expect(x.load(.seq_cst) & mask != 0);
289
290 // setting it again shouldn't change the bit
291 try testing.expectEqual(@as(u1, 1), x.bitSet(bit, .seq_cst));
292 try testing.expect(x.load(.seq_cst) & mask != 0);
293
294 // all the previous bits should have not changed (still be set)
295 for (0..bit_index) |prev_bit_index| {
296 const prev_bit = @as(std.math.Log2Int(usize), @intCast(prev_bit_index));
297 const prev_mask = @as(usize, 1) << prev_bit;
298 try testing.expect(x.load(.seq_cst) & prev_mask != 0);
299 }
300 }
301}
302
303test "Value.bitReset" {
304 var x = Value(usize).init(0);
305
306 for (0..@bitSizeOf(usize)) |bit_index| {
307 const bit = @as(std.math.Log2Int(usize), @intCast(bit_index));
308 const mask = @as(usize, 1) << bit;
309 x.raw |= mask;
310
311 // unsetting the bit should change the bit
312 try testing.expect(x.load(.seq_cst) & mask != 0);
313 try testing.expectEqual(@as(u1, 1), x.bitReset(bit, .seq_cst));
314 try testing.expect(x.load(.seq_cst) & mask == 0);
315
316 // unsetting it again shouldn't change the bit
317 try testing.expectEqual(@as(u1, 0), x.bitReset(bit, .seq_cst));
318 try testing.expect(x.load(.seq_cst) & mask == 0);
319
320 // all the previous bits should have not changed (still be reset)
321 for (0..bit_index) |prev_bit_index| {
322 const prev_bit = @as(std.math.Log2Int(usize), @intCast(prev_bit_index));
323 const prev_mask = @as(usize, 1) << prev_bit;
324 try testing.expect(x.load(.seq_cst) & prev_mask == 0);
325 }
326 }
327}
328
329test "Value.bitToggle" {
330 var x = Value(usize).init(0);
331
332 for (0..@bitSizeOf(usize)) |bit_index| {
333 const bit = @as(std.math.Log2Int(usize), @intCast(bit_index));
334 const mask = @as(usize, 1) << bit;
335
336 // toggling the bit should change the bit
337 try testing.expect(x.load(.seq_cst) & mask == 0);
338 try testing.expectEqual(@as(u1, 0), x.bitToggle(bit, .seq_cst));
339 try testing.expect(x.load(.seq_cst) & mask != 0);
340
341 // toggling it again *should* change the bit
342 try testing.expectEqual(@as(u1, 1), x.bitToggle(bit, .seq_cst));
343 try testing.expect(x.load(.seq_cst) & mask == 0);
344
345 // all the previous bits should have not changed (still be toggled back)
346 for (0..bit_index) |prev_bit_index| {
347 const prev_bit = @as(std.math.Log2Int(usize), @intCast(prev_bit_index));
348 const prev_mask = @as(usize, 1) << prev_bit;
349 try testing.expect(x.load(.seq_cst) & prev_mask == 0);
350 }
351 }
352}
353
354/// Signals to the processor that the caller is inside a busy-wait spin-loop.
355pub inline fn spinLoopHint() void {
356 switch (builtin.target.cpu.arch) {
357 // No-op instruction that can hint to save (or share with a hardware-thread)
358 // pipelining/power resources
359 // https://software.intel.com/content/www/us/en/develop/articles/benefitting-power-and-performance-sleep-loops.html
360 .x86,
361 .x86_64,
362 => asm volatile ("pause"),
363
364 // No-op instruction that serves as a hardware-thread resource yield hint.
365 // https://stackoverflow.com/a/7588941
366 .powerpc,
367 .powerpcle,
368 .powerpc64,
369 .powerpc64le,
370 => asm volatile ("or 27, 27, 27"),
371
372 // `isb` appears more reliable for releasing execution resources than `yield`
373 // on common aarch64 CPUs.
374 // https://bugs.java.com/bugdatabase/view_bug.do?bug_id=8258604
375 // https://bugs.mysql.com/bug.php?id=100664
376 .aarch64,
377 .aarch64_be,
378 => asm volatile ("isb"),
379
380 // https://www.keil.com/support/man/docs/armasm/armasm_dom1361289926796.htm
381 .arm,
382 .armeb,
383 => if (comptime builtin.cpu.has(.arm, .has_v6k)) {
384 asm volatile ("yield");
385 },
386
387 .thumb,
388 .thumbeb,
389 => if (comptime builtin.cpu.hasAny(.arm, &.{ .has_v6m, .thumb2 })) {
390 asm volatile ("yield");
391 },
392
393 // The 8-bit immediate specifies the amount of cycles to pause for. We can't really be too
394 // opinionated here.
395 .hexagon,
396 => asm volatile ("pause(#1)"),
397
398 .mips,
399 .mipsel,
400 .mips64,
401 .mips64el,
402 => if (comptime builtin.cpu.has(.mips, .mips32r2)) {
403 asm volatile ("pause");
404 },
405
406 .riscv32,
407 .riscv32be,
408 .riscv64,
409 .riscv64be,
410 => if (comptime builtin.cpu.has(.riscv, .zihintpause)) {
411 asm volatile ("pause");
412 },
413
414 .sparc,
415 .sparc64,
416 => if (comptime builtin.cpu.hasAny(.sparc, &.{ .v8plus, .v9 })) {
417 asm volatile ("rd %%ccr, %%g0");
418 },
419
420 else => {},
421 }
422}
423
424test spinLoopHint {
425 for (0..10) |_| {
426 spinLoopHint();
427 }
428}
429
430pub fn cacheLineForCpu(cpu: std.Target.Cpu) u16 {
431 return switch (cpu.arch) {
432 // x86_64: Starting from Intel's Sandy Bridge, the spatial prefetcher pulls in pairs of 64-byte cache lines at a time.
433 // - https://www.intel.com/content/dam/www/public/us/en/documents/manuals/64-ia-32-architectures-optimization-manual.pdf
434 // - https://github.com/facebook/folly/blob/1b5288e6eea6df074758f877c849b6e73bbb9fbb/folly/lang/Align.h#L107
435 //
436 // aarch64: Some big.LITTLE ARM archs have "big" cores with 128-byte cache lines:
437 // - https://www.mono-project.com/news/2016/09/12/arm64-icache/
438 // - https://cpufun.substack.com/p/more-m1-fun-hardware-information
439 //
440 // - https://github.com/torvalds/linux/blob/3a7e02c040b130b5545e4b115aada7bacd80a2b6/arch/arc/Kconfig#L212
441 // - https://github.com/golang/go/blob/3dd58676054223962cd915bb0934d1f9f489d4d2/src/internal/cpu/cpu_ppc64x.go#L9
442 .x86_64,
443 .aarch64,
444 .aarch64_be,
445 .arc,
446 .arceb,
447 .powerpc64,
448 .powerpc64le,
449 => 128,
450
451 // https://github.com/llvm/llvm-project/blob/e379094328e49731a606304f7e3559d4f1fa96f9/clang/lib/Basic/Targets/Hexagon.h#L145-L151
452 .hexagon,
453 => if (cpu.has(.hexagon, .v73)) 64 else 32,
454
455 // - https://github.com/golang/go/blob/3dd58676054223962cd915bb0934d1f9f489d4d2/src/internal/cpu/cpu_arm.go#L7
456 // - https://github.com/golang/go/blob/3dd58676054223962cd915bb0934d1f9f489d4d2/src/internal/cpu/cpu_mips.go#L7
457 // - https://github.com/golang/go/blob/3dd58676054223962cd915bb0934d1f9f489d4d2/src/internal/cpu/cpu_mipsle.go#L7
458 // - https://github.com/golang/go/blob/3dd58676054223962cd915bb0934d1f9f489d4d2/src/internal/cpu/cpu_mips64x.go#L9
459 // - https://github.com/torvalds/linux/blob/3a7e02c040b130b5545e4b115aada7bacd80a2b6/arch/sparc/include/asm/cache.h#L14
460 // - https://github.com/torvalds/linux/blob/3a7e02c040b130b5545e4b115aada7bacd80a2b6/arch/microblaze/include/asm/cache.h#L15
461 // - https://github.com/torvalds/linux/blob/3a7e02c040b130b5545e4b115aada7bacd80a2b6/arch/sh/include/cpu-sh4/cpu/cache.h#L10
462 // - https://github.com/openbsd/src/blob/1957873d2063db11dab780eca75b5e629d1e838d/sys/arch/m88k/m88k/atomic.S#L22
463 .arm,
464 .armeb,
465 .thumb,
466 .thumbeb,
467 .m88k,
468 .microblaze,
469 .microblazeel,
470 .mips,
471 .mipsel,
472 .mips64,
473 .mips64el,
474 .sh,
475 .sheb,
476 .sparc,
477 .sparc64,
478 => 32,
479
480 // - https://github.com/torvalds/linux/blob/3a7e02c040b130b5545e4b115aada7bacd80a2b6/arch/m68k/include/asm/cache.h#L10
481 // - https://github.com/torvalds/linux/blob/3a7e02c040b130b5545e4b115aada7bacd80a2b6/arch/openrisc/include/asm/cache.h#L24
482 // - https://github.com/torvalds/linux/blob/3a7e02c040b130b5545e4b115aada7bacd80a2b6/arch/parisc/include/asm/cache.h#L16
483 .hppa,
484 .hppa64,
485 .m68k,
486 .or1k,
487 => 16,
488
489 // - https://www.ti.com/lit/pdf/slaa498
490 .msp430,
491 => 8,
492
493 // - https://github.com/golang/go/blob/3dd58676054223962cd915bb0934d1f9f489d4d2/src/internal/cpu/cpu_s390x.go#L7
494 // - https://sxauroratsubasa.sakura.ne.jp/documents/guide/pdfs/Aurora_ISA_guide.pdf
495 .s390x,
496 .ve,
497 => 256,
498
499 // Other x86 and WASM platforms have 64-byte cache lines.
500 // The rest of the architectures are assumed to be similar.
501 // - https://github.com/golang/go/blob/dda2991c2ea0c5914714469c4defc2562a907230/src/internal/cpu/cpu_x86.go#L9
502 // - https://github.com/golang/go/blob/0a9321ad7f8c91e1b0c7184731257df923977eb9/src/internal/cpu/cpu_loong64.go#L11
503 // - https://github.com/golang/go/blob/3dd58676054223962cd915bb0934d1f9f489d4d2/src/internal/cpu/cpu_wasm.go#L7
504 // - https://github.com/golang/go/blob/19e923182e590ae6568c2c714f20f32512aeb3e3/src/internal/cpu/cpu_riscv64.go#L7
505 // - https://github.com/torvalds/linux/blob/3a7e02c040b130b5545e4b115aada7bacd80a2b6/arch/xtensa/variants/csp/include/variant/core.h#L209
506 // - https://github.com/torvalds/linux/blob/3a7e02c040b130b5545e4b115aada7bacd80a2b6/arch/csky/Kconfig#L183
507 // - https://github.com/torvalds/linux/blob/3a7e02c040b130b5545e4b115aada7bacd80a2b6/arch/alpha/include/asm/cache.h#L11
508 // - https://www.xmos.com/download/The-XMOS-XS3-Architecture.pdf
509 else => 64,
510 };
511}
512
513/// The estimated size of the CPU's cache line when atomically updating memory.
514/// Add this much padding or align to this boundary to avoid atomically-updated
515/// memory from forcing cache invalidations on near, but non-atomic, memory.
516///
517/// https://en.wikipedia.org/wiki/False_sharing
518/// https://github.com/golang/go/search?q=CacheLinePadSize
519pub const cache_line: comptime_int = cacheLineForCpu(builtin.cpu);
520
521test "current CPU has a cache line size" {
522 _ = cache_line;
523}
524
525/// A lock-free single-owner resource.
526pub const Mutex = enum(u8) {
527 unlocked,
528 locked,
529
530 pub fn tryLock(m: *Mutex) bool {
531 return @cmpxchgStrong(Mutex, m, .unlocked, .locked, .acquire, .monotonic) == null;
532 }
533
534 pub fn unlock(m: *Mutex) void {
535 assert(@atomicLoad(Mutex, m, .unordered) == .locked);
536 @atomicStore(Mutex, m, .unlocked, .release);
537 }
538};