diff --git a/src/benchmark/Akka.Benchmarks/Utils/FastLazyBenchmarks.cs b/src/benchmark/Akka.Benchmarks/Utils/FastLazyBenchmarks.cs index fdfff7bb4df..4cda7e7786e 100644 --- a/src/benchmark/Akka.Benchmarks/Utils/FastLazyBenchmarks.cs +++ b/src/benchmark/Akka.Benchmarks/Utils/FastLazyBenchmarks.cs @@ -19,8 +19,6 @@ public class FastLazyBenchmarks private Lazy lazySafe; private Lazy lazyUnsafe; private FastLazy fastLazy; - private FastLazy fastLazyWithInit; - [GlobalSetup] public void Setup() @@ -28,7 +26,6 @@ public void Setup() lazySafe = new Lazy(() => 100, LazyThreadSafetyMode.ExecutionAndPublication); lazyUnsafe = new Lazy(() => 100, LazyThreadSafetyMode.None); fastLazy = new FastLazy(() => 100); - fastLazyWithInit = new FastLazy(state => state + 100, 1000); } [Benchmark(Baseline = true)] @@ -48,11 +45,5 @@ public int FastLazy_get_value() { return fastLazy.Value; } - - [Benchmark] - public int FastLazy_stateful_get_value() - { - return fastLazyWithInit.Value; - } } } diff --git a/src/core/Akka.API.Tests/verify/CoreAPISpec.ApproveCore.DotNet.verified.txt b/src/core/Akka.API.Tests/verify/CoreAPISpec.ApproveCore.DotNet.verified.txt index 22b5545818d..0e3aabbad8d 100644 --- a/src/core/Akka.API.Tests/verify/CoreAPISpec.ApproveCore.DotNet.verified.txt +++ b/src/core/Akka.API.Tests/verify/CoreAPISpec.ApproveCore.DotNet.verified.txt @@ -4996,14 +4996,8 @@ namespace Akka.Util public sealed class FastLazy { public FastLazy(System.Func producer) { } - public bool IsValueCreated { get; } - public T Value { get; } - } - public sealed class FastLazy - { - public FastLazy(System.Func producer, S state) { } - public bool IsValueCreated { get; } public T Value { get; } + public bool IsValueCreated() { } } public interface ISurrogate { diff --git a/src/core/Akka.API.Tests/verify/CoreAPISpec.ApproveCore.Net.verified.txt b/src/core/Akka.API.Tests/verify/CoreAPISpec.ApproveCore.Net.verified.txt index b095dc3d8de..94cf12b14ed 100644 --- a/src/core/Akka.API.Tests/verify/CoreAPISpec.ApproveCore.Net.verified.txt +++ b/src/core/Akka.API.Tests/verify/CoreAPISpec.ApproveCore.Net.verified.txt @@ -4989,14 +4989,8 @@ namespace Akka.Util public sealed class FastLazy { public FastLazy(System.Func producer) { } - public bool IsValueCreated { get; } - public T Value { get; } - } - public sealed class FastLazy - { - public FastLazy(System.Func producer, S state) { } - public bool IsValueCreated { get; } public T Value { get; } + public bool IsValueCreated() { } } public interface ISurrogate { diff --git a/src/core/Akka.Tests/Util/FastLazySpecs.cs b/src/core/Akka.Tests/Util/FastLazySpecs.cs new file mode 100644 index 00000000000..374528b5315 --- /dev/null +++ b/src/core/Akka.Tests/Util/FastLazySpecs.cs @@ -0,0 +1,118 @@ +//----------------------------------------------------------------------- +// +// Copyright (C) 2009-2023 Lightbend Inc. +// Copyright (C) 2013-2023 .NET Foundation +// +//----------------------------------------------------------------------- + +using System; +using System.Collections.Concurrent; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Akka.Util; +using Xunit; + +namespace Akka.Tests.Util; + +public class FastLazySpecs +{ + [Fact] + public void FastLazy_should_indicate_no_value_has_been_produced() + { + var fal = new FastLazy(() => 2); + Assert.False(fal.IsValueCreated()); + } + + [Fact] + public void FastLazy_should_produce_value() + { + var fal = new FastLazy(() => 2); + var value = fal.Value; + Assert.Equal(2, value); + Assert.True(fal.IsValueCreated()); + } + + [Fact] + public void FastLazy_must_be_threadsafe() + { + for (var c = 0; c < 100000; c++) // try this 100000 times + { + var values = new ConcurrentBag(); + var fal = new FastLazy(() => new Random().Next(1, Int32.MaxValue)); + var result = Parallel.For(0, 1000, i => values.Add(fal.Value)); // 1000 concurrent operations + SpinWait.SpinUntil(() => result.IsCompleted); + var value = values.First(); + Assert.NotEqual(0, value); + Assert.True(values.All(x => x.Equals(value))); + } + } + + [Fact] + public void FastLazy_only_single_value_creation_attempt() + { + int attempts = 0; + Func slowValueFactory = () => + { + Interlocked.Increment(ref attempts); + Thread.Sleep(100); + return new Random().Next(1, Int32.MaxValue); + }; + + var values = new ConcurrentBag(); + var fal = new FastLazy(slowValueFactory); + var result = Parallel.For(0, 1000, i => values.Add(fal.Value)); // 1000 concurrent operations + SpinWait.SpinUntil(() => result.IsCompleted); + var value = values.First(); + Assert.NotEqual(0, value); + Assert.True(values.All(x => x.Equals(value))); + Assert.Equal(1000, values.Count); + Assert.Equal(1, attempts); + } + + [Fact] + public void FastLazy_must_be_threadsafe_AnyRef() + { + for (var c = 0; c < 100000; c++) // try this 100000 times + { + var values = new ConcurrentBag(); + var fal = new FastLazy(() => Guid.NewGuid().ToString()); + var result = Parallel.For(0, 1000, i => values.Add(fal.Value)); // 1000 concurrent operations + SpinWait.SpinUntil(() => result.IsCompleted); + var value = values.First(); + Assert.NotNull(value); + Assert.True(values.All(x => x.Equals(value))); + } + } + + [Fact] + public void FastLazy_only_single_value_creation_attempt_AnyRef() + { + int attempts = 0; + Func slowValueFactory = () => + { + Interlocked.Increment(ref attempts); + Thread.Sleep(100); + return Guid.NewGuid().ToString(); + }; + + var values = new ConcurrentBag(); + var fal = new FastLazy(slowValueFactory); + var result = Parallel.For(0, 1000, i => values.Add(fal.Value)); // 1000 concurrent operations + SpinWait.SpinUntil(() => result.IsCompleted); + var value = values.First(); + Assert.NotNull(value); + Assert.True(values.All(x => x.Equals(value))); + Assert.Equal(1000, values.Count); + Assert.Equal(1, attempts); + } + + [Fact] + public void FastLazy_AllThreads_ShouldThrowException_WhenFactoryThrowsException() + { + var lazy = new FastLazy(() => throw new Exception("Factory exception")); + var result = Parallel.For(0, 10, i => { Assert.Throws(() => _ = lazy.Value); }); + + Assert.True(result.IsCompleted); + } +} diff --git a/src/core/Akka/Util/FastLazy.cs b/src/core/Akka/Util/FastLazy.cs index 4f96375bb5d..1be692b5661 100644 --- a/src/core/Akka/Util/FastLazy.cs +++ b/src/core/Akka/Util/FastLazy.cs @@ -15,144 +15,55 @@ namespace Akka.Util /// A fast, atomic lazy that only allows a single publish operation to happen, /// but allows executions to occur concurrently. /// - /// Does not cache exceptions. Designed for use with types that are - /// or are otherwise considered to be expensive to allocate. - /// - /// Read the full explanation here: https://github.com/Aaronontheweb/FastAtomicLazy#rationale + /// Does not cache exceptions. Designed for use with types that are + /// or are otherwise considered to be expensive to allocate. Read the full explanation here: https://github.com/Aaronontheweb/FastAtomicLazy#rationale /// - /// TBD public sealed class FastLazy { - private readonly Func _producer; - private byte _created = 0; - private byte _creating = 0; + private Func _producer; + private int _status = 0; + private Exception _exception; private T _createdValue; - /// - /// Initializes a new instance of the class. - /// - /// - /// This exception is thrown if the given is undefined. - /// public FastLazy(Func producer) { - if (producer == null) throw new ArgumentNullException(nameof(producer), "Producer cannot be null"); - _producer = producer; + _producer = producer ?? throw new ArgumentNullException(nameof(producer)); } - /// - /// TBD - /// - /// TBD - public bool IsValueCreated => IsValueCreatedInternal(); - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private bool IsValueCreatedInternal() - { - return Volatile.Read(ref _created) == 1; - } - + public bool IsValueCreated() => Volatile.Read(ref _status) == 2; + [MethodImpl(MethodImplOptions.AggressiveInlining)] - private bool IsValueCreationInProgress() - { - return Volatile.Read(ref _creating) == 1; - } - - /// - /// TBD - /// + private bool IsExceptionThrown() => Volatile.Read(ref _exception) != null; + public T Value { get { - if (IsValueCreatedInternal()) + if (IsValueCreated()) return _createdValue; - if (!IsValueCreationInProgress()) - { - Volatile.Write(ref _creating, 1); - _createdValue = _producer(); - Volatile.Write(ref _created, 1); - } - else + + if (Interlocked.CompareExchange(ref _status, 1, 0) == 0) { - SpinWait.SpinUntil(IsValueCreatedInternal); - } - return _createdValue; - } - } - } - - - /// - /// A fast, atomic lazy that only allows a single publish operation to happen, - /// but allows executions to occur concurrently. - /// - /// Does not cache exceptions. Designed for use with types that are - /// or are otherwise considered to be expensive to allocate. - /// - /// Read the full explanation here: https://github.com/Aaronontheweb/FastAtomicLazy#rationale - /// - /// State type - /// Value type - public sealed class FastLazy - { - private readonly Func _producer; - private byte _created = 0; - private byte _creating = 0; - private T _createdValue; - private S _state; - - /// - /// Initializes a new instance of the class. - /// - /// - /// This exception is thrown if the given or is undefined. - /// - public FastLazy(Func producer, S state) - { - if(producer == null) throw new ArgumentNullException(nameof(producer), "Producer cannot be null"); - if(state == null) throw new ArgumentNullException(nameof(state), "State cannot be null"); - _producer = producer; - _state = state; - } - - /// - /// TBD - /// - /// TBD - public bool IsValueCreated => IsValueCreatedInternal(); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private bool IsValueCreatedInternal() - { - return Volatile.Read(ref _created) == 1; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private bool IsValueCreationInProgress() - { - return Volatile.Read(ref _creating) == 1; - } + try + { + _createdValue = _producer(); + } + catch (Exception e) + { + Volatile.Write(ref _exception, e); + throw; + } - /// - /// TBD - /// - public T Value - { - get - { - if (IsValueCreatedInternal()) - return _createdValue; - if (!IsValueCreationInProgress()) - { - Volatile.Write(ref _creating, 1); - _createdValue = _producer(_state); - Volatile.Write(ref _created, 1); - _state = default(S); // for reference types to make it suitable for gc + Volatile.Write(ref _status, 2); + _producer = null; // release for GC } else { - SpinWait.SpinUntil(IsValueCreatedInternal); + SpinWait.SpinUntil(() => IsValueCreated() || IsExceptionThrown()); + var e = Volatile.Read(ref _exception); + if (e != null) + throw e; } return _createdValue; }