diff --git a/levsync/levsync.odin b/levsync/levsync.odin index 26e4f75..f8c1d44 100644 --- a/levsync/levsync.odin +++ b/levsync/levsync.odin @@ -189,10 +189,10 @@ import "core:sync" import "core:testing" import "core:thread" +// Multiple threads will each add 1.0 this many times. +// If any updates are lost due to race conditions, the final sum will be wrong. @(test) test_concurrent_atomic_add_no_lost_updates :: proc(t: ^testing.T) { - // Multiple threads will each add 1.0 this many times. - // If any updates are lost due to race conditions, the final sum will be wrong. NUM_THREADS :: 8 ITERATIONS_PER_THREAD :: 10_000 @@ -234,10 +234,10 @@ test_concurrent_atomic_add_no_lost_updates :: proc(t: ^testing.T) { testing.expect_value(t, shared_value, expected) } +// Start with a known value, multiple threads subtract. +// If any updates are lost due to race conditions, the final result will be wrong. @(test) test_concurrent_atomic_sub_no_lost_updates :: proc(t: ^testing.T) { - // Start with a known value, multiple threads subtract. - // If any updates are lost due to race conditions, the final result will be wrong. NUM_THREADS :: 8 ITERATIONS_PER_THREAD :: 10_000 @@ -278,11 +278,11 @@ test_concurrent_atomic_sub_no_lost_updates :: proc(t: ^testing.T) { testing.expect_value(t, shared_value, 0.0) } +// Each thread multiplies by 2.0 then divides by 2.0. +// Since these are inverses, the final value should equal the starting value +// regardless of how operations interleave. @(test) test_concurrent_atomic_mul_div_round_trip :: proc(t: ^testing.T) { - // Each thread multiplies by 2.0 then divides by 2.0. - // Since these are inverses, the final value should equal the starting value - // regardless of how operations interleave. NUM_THREADS :: 8 ITERATIONS_PER_THREAD :: 10_000 @@ -324,10 +324,10 @@ test_concurrent_atomic_mul_div_round_trip :: proc(t: ^testing.T) { testing.expect_value(t, shared_value, 1000.0) } +// Verify the f32 type dispatch works correctly under contention. +// Same approach as the f64 add test but with f32. @(test) test_atomic_add_with_f32 :: proc(t: ^testing.T) { - // Verify the f32 type dispatch works correctly under contention. - // Same approach as the f64 add test but with f32. NUM_THREADS :: 8 ITERATIONS_PER_THREAD :: 10_000 @@ -369,17 +369,17 @@ test_atomic_add_with_f32 :: proc(t: ^testing.T) { testing.expect_value(t, shared_value, expected) } +// Tests that the memory order passed to atomic_float_op's CAS success condition +// provides full ordering guarantees for the entire float operation. +// +// Both sides use atomic_add_float (not raw intrinsics) to verify: +// - Release on CAS success publishes prior non-atomic writes +// - Acquire on CAS success makes those writes visible to the reader +// +// NOTE: This test may pass even with Relaxed ordering on x86 due to its strong memory model. +// On ARM or other weak-memory architectures, using Relaxed here would likely cause failures. @(test) test_atomic_release_acquire_publish_visibility :: proc(t: ^testing.T) { - // Tests that the memory order passed to atomic_float_op's CAS success condition - // provides full ordering guarantees for the entire float operation. - // - // Both sides use atomic_add_float (not raw intrinsics) to verify: - // - Release on CAS success publishes prior non-atomic writes - // - Acquire on CAS success makes those writes visible to the reader - // - // NOTE: This test may pass even with Relaxed ordering on x86 due to its strong memory model. - // On ARM or other weak-memory architectures, using Relaxed here would likely cause failures. NUM_READERS :: 4 Shared_State :: struct { @@ -476,20 +476,20 @@ test_atomic_release_acquire_publish_visibility :: proc(t: ^testing.T) { } } +// Stress test for every spinlock acquisition variant: N threads contend on a +// single lock and perform a deliberate non-atomic read-modify-write on shared +// data. Each iteration rotates through spinlock_try_lock, spinlock_lock, +// spinlock_guard, and spinlock_tryguard so every variant runs concurrently and +// must uphold mutual exclusion on the same lock. +// +// If mutual exclusion holds: +// - `counter` ends at exactly NUM_THREADS * ITERATIONS_PER_THREAD +// - `concurrent_holders` never exceeds 1 +// +// A multi-step RMW (read → relax → write) widens the critical section so +// any failure to exclude is virtually guaranteed to corrupt the counter. @(test) test_spinlock_mutual_exclusion :: proc(t: ^testing.T) { - // Stress test for every spinlock acquisition variant: N threads contend on a - // single lock and perform a deliberate non-atomic read-modify-write on shared - // data. Each iteration rotates through spinlock_try_lock, spinlock_lock, - // spinlock_guard, and spinlock_tryguard so every variant runs concurrently and - // must uphold mutual exclusion on the same lock. - // - // If mutual exclusion holds: - // - `counter` ends at exactly NUM_THREADS * ITERATIONS_PER_THREAD - // - `concurrent_holders` never exceeds 1 - // - // A multi-step RMW (read → relax → write) widens the critical section so - // any failure to exclude is virtually guaranteed to corrupt the counter. NUM_THREADS :: 8 ITERATIONS_PER_THREAD :: 50_000 @@ -560,20 +560,18 @@ test_spinlock_mutual_exclusion :: proc(t: ^testing.T) { spinlock_lock(&s.lock) critical_section(s) spinlock_unlock(&s.lock) - case 2: - // Scoped guard: unlocks automatically at the end of the block. - if spinlock_guard(&s.lock) { - critical_section(s) - } - case 3: - // Scoped try-guard: retry until acquired, auto-unlocks on success. - for { - if spinlock_tryguard(&s.lock) { + case 2: // Scoped guard: unlocks automatically at the end of the block. + if spinlock_guard(&s.lock) { critical_section(s) - break } - intrinsics.cpu_relax() - } + case 3: // Scoped try-guard: retry until acquired, auto-unlocks on success. + for { + if spinlock_tryguard(&s.lock) { + critical_section(s) + break + } + intrinsics.cpu_relax() + } } } }