Skip to content

Commit c2559ff

Browse files
F0b0sAaronontheweb
andauthored
Fix FastLazy race condition and waiting thread hanging (#6336) (#6707)
* Fix FastLazy race condition and waiting thread hanging (#6336). * Move properties to methods. * Fix verified file. * Fix verified file. --------- Co-authored-by: Aaron Stannard <[email protected]>
1 parent 541ffc9 commit c2559ff

File tree

5 files changed

+148
-140
lines changed

5 files changed

+148
-140
lines changed

src/benchmark/Akka.Benchmarks/Utils/FastLazyBenchmarks.cs

-9
Original file line numberDiff line numberDiff line change
@@ -19,16 +19,13 @@ public class FastLazyBenchmarks
1919
private Lazy<int> lazySafe;
2020
private Lazy<int> lazyUnsafe;
2121
private FastLazy<int> fastLazy;
22-
private FastLazy<int, int> fastLazyWithInit;
23-
2422

2523
[GlobalSetup]
2624
public void Setup()
2725
{
2826
lazySafe = new Lazy<int>(() => 100, LazyThreadSafetyMode.ExecutionAndPublication);
2927
lazyUnsafe = new Lazy<int>(() => 100, LazyThreadSafetyMode.None);
3028
fastLazy = new FastLazy<int>(() => 100);
31-
fastLazyWithInit = new FastLazy<int, int>(state => state + 100, 1000);
3229
}
3330

3431
[Benchmark(Baseline = true)]
@@ -48,11 +45,5 @@ public int FastLazy_get_value()
4845
{
4946
return fastLazy.Value;
5047
}
51-
52-
[Benchmark]
53-
public int FastLazy_stateful_get_value()
54-
{
55-
return fastLazyWithInit.Value;
56-
}
5748
}
5849
}

src/core/Akka.API.Tests/verify/CoreAPISpec.ApproveCore.DotNet.verified.txt

+1-7
Original file line numberDiff line numberDiff line change
@@ -4996,14 +4996,8 @@ namespace Akka.Util
49964996
public sealed class FastLazy<T>
49974997
{
49984998
public FastLazy(System.Func<T> producer) { }
4999-
public bool IsValueCreated { get; }
5000-
public T Value { get; }
5001-
}
5002-
public sealed class FastLazy<S, T>
5003-
{
5004-
public FastLazy(System.Func<S, T> producer, S state) { }
5005-
public bool IsValueCreated { get; }
50064999
public T Value { get; }
5000+
public bool IsValueCreated() { }
50075001
}
50085002
public interface ISurrogate
50095003
{

src/core/Akka.API.Tests/verify/CoreAPISpec.ApproveCore.Net.verified.txt

+1-7
Original file line numberDiff line numberDiff line change
@@ -4989,14 +4989,8 @@ namespace Akka.Util
49894989
public sealed class FastLazy<T>
49904990
{
49914991
public FastLazy(System.Func<T> producer) { }
4992-
public bool IsValueCreated { get; }
4993-
public T Value { get; }
4994-
}
4995-
public sealed class FastLazy<S, T>
4996-
{
4997-
public FastLazy(System.Func<S, T> producer, S state) { }
4998-
public bool IsValueCreated { get; }
49994992
public T Value { get; }
4993+
public bool IsValueCreated() { }
50004994
}
50014995
public interface ISurrogate
50024996
{
+118
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
//-----------------------------------------------------------------------
2+
// <copyright file="FastLazySpecs.cs" company="Akka.NET Project">
3+
// Copyright (C) 2009-2023 Lightbend Inc. <http://www.lightbend.com>
4+
// Copyright (C) 2013-2023 .NET Foundation <https://github.com/akkadotnet/akka.net>
5+
// </copyright>
6+
//-----------------------------------------------------------------------
7+
8+
using System;
9+
using System.Collections.Concurrent;
10+
using System.Linq;
11+
using System.Threading;
12+
using System.Threading.Tasks;
13+
using Akka.Util;
14+
using Xunit;
15+
16+
namespace Akka.Tests.Util;
17+
18+
public class FastLazySpecs
19+
{
20+
[Fact]
21+
public void FastLazy_should_indicate_no_value_has_been_produced()
22+
{
23+
var fal = new FastLazy<int>(() => 2);
24+
Assert.False(fal.IsValueCreated());
25+
}
26+
27+
[Fact]
28+
public void FastLazy_should_produce_value()
29+
{
30+
var fal = new FastLazy<int>(() => 2);
31+
var value = fal.Value;
32+
Assert.Equal(2, value);
33+
Assert.True(fal.IsValueCreated());
34+
}
35+
36+
[Fact]
37+
public void FastLazy_must_be_threadsafe()
38+
{
39+
for (var c = 0; c < 100000; c++) // try this 100000 times
40+
{
41+
var values = new ConcurrentBag<int>();
42+
var fal = new FastLazy<int>(() => new Random().Next(1, Int32.MaxValue));
43+
var result = Parallel.For(0, 1000, i => values.Add(fal.Value)); // 1000 concurrent operations
44+
SpinWait.SpinUntil(() => result.IsCompleted);
45+
var value = values.First();
46+
Assert.NotEqual(0, value);
47+
Assert.True(values.All(x => x.Equals(value)));
48+
}
49+
}
50+
51+
[Fact]
52+
public void FastLazy_only_single_value_creation_attempt()
53+
{
54+
int attempts = 0;
55+
Func<int> slowValueFactory = () =>
56+
{
57+
Interlocked.Increment(ref attempts);
58+
Thread.Sleep(100);
59+
return new Random().Next(1, Int32.MaxValue);
60+
};
61+
62+
var values = new ConcurrentBag<int>();
63+
var fal = new FastLazy<int>(slowValueFactory);
64+
var result = Parallel.For(0, 1000, i => values.Add(fal.Value)); // 1000 concurrent operations
65+
SpinWait.SpinUntil(() => result.IsCompleted);
66+
var value = values.First();
67+
Assert.NotEqual(0, value);
68+
Assert.True(values.All(x => x.Equals(value)));
69+
Assert.Equal(1000, values.Count);
70+
Assert.Equal(1, attempts);
71+
}
72+
73+
[Fact]
74+
public void FastLazy_must_be_threadsafe_AnyRef()
75+
{
76+
for (var c = 0; c < 100000; c++) // try this 100000 times
77+
{
78+
var values = new ConcurrentBag<string>();
79+
var fal = new FastLazy<string>(() => Guid.NewGuid().ToString());
80+
var result = Parallel.For(0, 1000, i => values.Add(fal.Value)); // 1000 concurrent operations
81+
SpinWait.SpinUntil(() => result.IsCompleted);
82+
var value = values.First();
83+
Assert.NotNull(value);
84+
Assert.True(values.All(x => x.Equals(value)));
85+
}
86+
}
87+
88+
[Fact]
89+
public void FastLazy_only_single_value_creation_attempt_AnyRef()
90+
{
91+
int attempts = 0;
92+
Func<string> slowValueFactory = () =>
93+
{
94+
Interlocked.Increment(ref attempts);
95+
Thread.Sleep(100);
96+
return Guid.NewGuid().ToString();
97+
};
98+
99+
var values = new ConcurrentBag<string>();
100+
var fal = new FastLazy<string>(slowValueFactory);
101+
var result = Parallel.For(0, 1000, i => values.Add(fal.Value)); // 1000 concurrent operations
102+
SpinWait.SpinUntil(() => result.IsCompleted);
103+
var value = values.First();
104+
Assert.NotNull(value);
105+
Assert.True(values.All(x => x.Equals(value)));
106+
Assert.Equal(1000, values.Count);
107+
Assert.Equal(1, attempts);
108+
}
109+
110+
[Fact]
111+
public void FastLazy_AllThreads_ShouldThrowException_WhenFactoryThrowsException()
112+
{
113+
var lazy = new FastLazy<string>(() => throw new Exception("Factory exception"));
114+
var result = Parallel.For(0, 10, i => { Assert.Throws<Exception>(() => _ = lazy.Value); });
115+
116+
Assert.True(result.IsCompleted);
117+
}
118+
}

src/core/Akka/Util/FastLazy.cs

+28-117
Original file line numberDiff line numberDiff line change
@@ -15,144 +15,55 @@ namespace Akka.Util
1515
/// A fast, atomic lazy that only allows a single publish operation to happen,
1616
/// but allows executions to occur concurrently.
1717
///
18-
/// Does not cache exceptions. Designed for use with <typeparamref name="T"/> types that are <see cref="IDisposable"/>
19-
/// or are otherwise considered to be expensive to allocate.
20-
///
21-
/// Read the full explanation here: https://github.com/Aaronontheweb/FastAtomicLazy#rationale
18+
/// Does not cache exceptions. Designed for use with <typeparam name="T"/> types that are <see cref="IDisposable"/>
19+
/// or are otherwise considered to be expensive to allocate. Read the full explanation here: https://github.com/Aaronontheweb/FastAtomicLazy#rationale
2220
/// </summary>
23-
/// <typeparam name="T">TBD</typeparam>
2421
public sealed class FastLazy<T>
2522
{
26-
private readonly Func<T> _producer;
27-
private byte _created = 0;
28-
private byte _creating = 0;
23+
private Func<T> _producer;
24+
private int _status = 0;
25+
private Exception _exception;
2926
private T _createdValue;
3027

31-
/// <summary>
32-
/// Initializes a new instance of the <see cref="FastLazy{T}"/> class.
33-
/// </summary>
34-
/// <exception cref="ArgumentNullException">
35-
/// This exception is thrown if the given <paramref name="producer"/> is undefined.
36-
/// </exception>
3728
public FastLazy(Func<T> producer)
3829
{
39-
if (producer == null) throw new ArgumentNullException(nameof(producer), "Producer cannot be null");
40-
_producer = producer;
30+
_producer = producer ?? throw new ArgumentNullException(nameof(producer));
4131
}
4232

43-
/// <summary>
44-
/// TBD
45-
/// </summary>
46-
/// <returns>TBD</returns>
47-
public bool IsValueCreated => IsValueCreatedInternal();
48-
4933
[MethodImpl(MethodImplOptions.AggressiveInlining)]
50-
private bool IsValueCreatedInternal()
51-
{
52-
return Volatile.Read(ref _created) == 1;
53-
}
54-
34+
public bool IsValueCreated() => Volatile.Read(ref _status) == 2;
35+
5536
[MethodImpl(MethodImplOptions.AggressiveInlining)]
56-
private bool IsValueCreationInProgress()
57-
{
58-
return Volatile.Read(ref _creating) == 1;
59-
}
60-
61-
/// <summary>
62-
/// TBD
63-
/// </summary>
37+
private bool IsExceptionThrown() => Volatile.Read(ref _exception) != null;
38+
6439
public T Value
6540
{
6641
get
6742
{
68-
if (IsValueCreatedInternal())
43+
if (IsValueCreated())
6944
return _createdValue;
70-
if (!IsValueCreationInProgress())
71-
{
72-
Volatile.Write(ref _creating, 1);
73-
_createdValue = _producer();
74-
Volatile.Write(ref _created, 1);
75-
}
76-
else
45+
46+
if (Interlocked.CompareExchange(ref _status, 1, 0) == 0)
7747
{
78-
SpinWait.SpinUntil(IsValueCreatedInternal);
79-
}
80-
return _createdValue;
81-
}
82-
}
83-
}
84-
85-
86-
/// <summary>
87-
/// A fast, atomic lazy that only allows a single publish operation to happen,
88-
/// but allows executions to occur concurrently.
89-
///
90-
/// Does not cache exceptions. Designed for use with <typeparamref name="T"/> types that are <see cref="IDisposable"/>
91-
/// or are otherwise considered to be expensive to allocate.
92-
///
93-
/// Read the full explanation here: https://github.com/Aaronontheweb/FastAtomicLazy#rationale
94-
/// </summary>
95-
/// <typeparam name="S">State type</typeparam>
96-
/// <typeparam name="T">Value type</typeparam>
97-
public sealed class FastLazy<S, T>
98-
{
99-
private readonly Func<S, T> _producer;
100-
private byte _created = 0;
101-
private byte _creating = 0;
102-
private T _createdValue;
103-
private S _state;
104-
105-
/// <summary>
106-
/// Initializes a new instance of the <see cref="FastLazy{T}"/> class.
107-
/// </summary>
108-
/// <exception cref="ArgumentNullException">
109-
/// This exception is thrown if the given <paramref name="producer"/> or <paramref name="state"/> is undefined.
110-
/// </exception>
111-
public FastLazy(Func<S, T> producer, S state)
112-
{
113-
if(producer == null) throw new ArgumentNullException(nameof(producer), "Producer cannot be null");
114-
if(state == null) throw new ArgumentNullException(nameof(state), "State cannot be null");
115-
_producer = producer;
116-
_state = state;
117-
}
118-
119-
/// <summary>
120-
/// TBD
121-
/// </summary>
122-
/// <returns>TBD</returns>
123-
public bool IsValueCreated => IsValueCreatedInternal();
124-
125-
[MethodImpl(MethodImplOptions.AggressiveInlining)]
126-
private bool IsValueCreatedInternal()
127-
{
128-
return Volatile.Read(ref _created) == 1;
129-
}
130-
131-
[MethodImpl(MethodImplOptions.AggressiveInlining)]
132-
private bool IsValueCreationInProgress()
133-
{
134-
return Volatile.Read(ref _creating) == 1;
135-
}
48+
try
49+
{
50+
_createdValue = _producer();
51+
}
52+
catch (Exception e)
53+
{
54+
Volatile.Write(ref _exception, e);
55+
throw;
56+
}
13657

137-
/// <summary>
138-
/// TBD
139-
/// </summary>
140-
public T Value
141-
{
142-
get
143-
{
144-
if (IsValueCreatedInternal())
145-
return _createdValue;
146-
if (!IsValueCreationInProgress())
147-
{
148-
Volatile.Write(ref _creating, 1);
149-
_createdValue = _producer(_state);
150-
Volatile.Write(ref _created, 1);
151-
_state = default(S); // for reference types to make it suitable for gc
58+
Volatile.Write(ref _status, 2);
59+
_producer = null; // release for GC
15260
}
15361
else
15462
{
155-
SpinWait.SpinUntil(IsValueCreatedInternal);
63+
SpinWait.SpinUntil(() => IsValueCreated() || IsExceptionThrown());
64+
var e = Volatile.Read(ref _exception);
65+
if (e != null)
66+
throw e;
15667
}
15768
return _createdValue;
15869
}

0 commit comments

Comments
 (0)