Skip to content

Commit c282cf4

Browse files
committed
Threading 5.15.0
1 parent 5bdb434 commit c282cf4

16 files changed

+548
-59
lines changed

CHANGELOG.md

+6
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
Release Notes
22
====
33

4+
# 10-16-2023
5+
<a href="https://www.nuget.org/packages/dotnext.threading/5.15.0">DotNext.Threading 5.15.0</a>
6+
* Added support of synchronous lock acquisition to `AsyncExclusiveLock`, `AsyncReaderWriterLock`, `AsyncManualResetEvent`, `AsyncAutoResetEvent` so the users can easily migrate step-by-step from monitors and other synchronization primitives to async-friendly primitives
7+
* Fixed random `InvalidOperationException` caused by `RandomAccessCache<TKey, TValue>`
8+
* Added synchronous methods to `RandomAccessCache<TKey, TValue>` to support [251](https://github.com/dotnet/dotNext/issues/251) feature request
9+
410
# 10-13-2024
511
<a href="https://www.nuget.org/packages/dotnext/5.14.0">DotNext 5.14.0</a>
612
* Added helpers to `DelegateHelpers` class to convert delegates with synchronous signature to their asynchronous counterparts

README.md

+5-25
Original file line numberDiff line numberDiff line change
@@ -44,32 +44,12 @@ All these things are implemented in 100% managed code on top of existing .NET AP
4444
* [NuGet Packages](https://www.nuget.org/profiles/rvsakno)
4545

4646
# What's new
47-
Release Date: 10-13-2024
47+
Release Date: 10-16-2024
4848

49-
<a href="https://www.nuget.org/packages/dotnext/5.14.0">DotNext 5.14.0</a>
50-
* Added helpers to `DelegateHelpers` class to convert delegates with synchronous signature to their asynchronous counterparts
51-
* Added support of async enumerator to `SingletonList<T>`
52-
* Fixed exception propagation in `DynamicTaskAwaitable`
53-
* Added support of [ConfigureAwaitOptions](https://learn.microsoft.com/en-us/dotnet/api/system.threading.tasks.configureawaitoptions) to `DynamicTaskAwaitable`
54-
55-
<a href="https://www.nuget.org/packages/dotnext.metaprogramming/5.14.0">DotNext.Metaprogramming 5.14.0</a>
56-
* Updated dependencies
57-
58-
<a href="https://www.nuget.org/packages/dotnext.unsafe/5.14.0">DotNext.Unsafe 5.14.0</a>
59-
* Updated dependencies
60-
61-
<a href="https://www.nuget.org/packages/dotnext.threading/5.14.0">DotNext.Threading 5.14.0</a>
62-
* Updated dependencies
63-
64-
<a href="https://www.nuget.org/packages/dotnext.io/5.14.0">DotNext.IO 5.14.0</a>
65-
* Updated dependencies
66-
67-
<a href="https://www.nuget.org/packages/dotnext.net.cluster/5.14.0">DotNext.Net.Cluster 5.14.0</a>
68-
* Fixed graceful shutdown of Raft TCP listener
69-
* Updated vulnerable dependencies
70-
71-
<a href="https://www.nuget.org/packages/dotnext.aspnetcore.cluster/5.14.0">DotNext.AspNetCore.Cluster 5.14.0</a>
72-
* Updated vulnerable dependencies
49+
<a href="https://www.nuget.org/packages/dotnext.threading/5.15.0">DotNext.Threading 5.15.0</a>
50+
* Added support of synchronous lock acquisition to `AsyncExclusiveLock`, `AsyncReaderWriterLock`, `AsyncManualResetEvent`, `AsyncAutoResetEvent` so the users can easily migrate step-by-step from monitors and other synchronization primitives to async-friendly primitives
51+
* Fixed random `InvalidOperationException` caused by `RandomAccessCache<TKey, TValue>`
52+
* Added synchronous methods to `RandomAccessCache<TKey, TValue>` to support [251](https://github.com/dotnet/dotNext/issues/251) feature request
7353

7454
Changelog for previous versions located [here](./CHANGELOG.md).
7555

src/DotNext.Tests/Runtime/Caching/RandomAccessCacheTests.cs

+37-2
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ static async Task RequestLoop(RandomAccessCache<long, string> cache)
105105
}
106106

107107
[Fact]
108-
public static async Task AddRemove()
108+
public static async Task AddRemoveAsync()
109109
{
110110
await using var cache = new RandomAccessCache<long, string>(15);
111111

@@ -122,9 +122,29 @@ public static async Task AddRemove()
122122
Equal("10", session.Value);
123123
}
124124
}
125+
126+
[Fact]
127+
public static void AddRemove()
128+
{
129+
using var cache = new RandomAccessCache<long, string>(15);
130+
131+
using (var writeSession = cache.Change(10L, DefaultTimeout))
132+
{
133+
False(writeSession.TryGetValue(out _));
134+
writeSession.SetValue("10");
135+
}
136+
137+
False(cache.TryRemove(11L, DefaultTimeout, out _));
138+
True(cache.TryRemove(10L, DefaultTimeout, out var session));
139+
140+
using (session)
141+
{
142+
Equal("10", session.Value);
143+
}
144+
}
125145

126146
[Fact]
127-
public static async Task AddInvalidate()
147+
public static async Task AddInvalidateAsync()
128148
{
129149
await using var cache = new RandomAccessCache<long, string>(15);
130150

@@ -137,6 +157,21 @@ public static async Task AddInvalidate()
137157
False(await cache.InvalidateAsync(11L));
138158
True(await cache.InvalidateAsync(10L));
139159
}
160+
161+
[Fact]
162+
public static void AddInvalidate()
163+
{
164+
using var cache = new RandomAccessCache<long, string>(15);
165+
166+
using (var session = cache.Change(10L, DefaultTimeout))
167+
{
168+
False(session.TryGetValue(out _));
169+
session.SetValue("10");
170+
}
171+
172+
False(cache.Invalidate(11L, DefaultTimeout));
173+
True(cache.Invalidate(10L, DefaultTimeout));
174+
}
140175

141176
[Fact]
142177
public static async Task AddTwice()

src/DotNext.Tests/Threading/AsyncExclusiveLockTests.cs

+35
Original file line numberDiff line numberDiff line change
@@ -179,4 +179,39 @@ public static async Task LockStealing2()
179179
l.Release();
180180
await task3;
181181
}
182+
183+
[Fact]
184+
public static void SynchronousLock()
185+
{
186+
using var l = new AsyncExclusiveLock();
187+
True(l.TryAcquire(DefaultTimeout));
188+
189+
False(l.TryAcquire(TimeSpan.Zero));
190+
}
191+
192+
[Fact]
193+
public static async Task MixedLock()
194+
{
195+
await using var l = new AsyncExclusiveLock();
196+
True(await l.TryAcquireAsync(DefaultTimeout));
197+
198+
var t = Task.Factory.StartNew(() => l.TryAcquire(DefaultTimeout), TaskCreationOptions.LongRunning);
199+
l.Release();
200+
201+
True(await t);
202+
False(l.TryAcquire());
203+
l.Release();
204+
}
205+
206+
[Fact]
207+
public static async Task DisposedWhenSynchronousLockAcquired()
208+
{
209+
var l = new AsyncExclusiveLock();
210+
True(l.TryAcquire());
211+
212+
var t = Task.Factory.StartNew(() => l.TryAcquire(DefaultTimeout), TaskCreationOptions.LongRunning);
213+
214+
l.Dispose();
215+
await ThrowsAsync<ObjectDisposedException>(Func.Constant(t));
216+
}
182217
}

src/DotNext.Tests/Threading/AsyncReaderWriterLockTests.cs

+62-1
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ public static async Task TrivialLock()
2424
True(await rwLock.TryEnterReadLockAsync(DefaultTimeout));
2525
True(await rwLock.TryUpgradeToWriteLockAsync(DefaultTimeout));
2626
False(rwLock.TryEnterWriteLock());
27+
False(rwLock.TryUpgradeToWriteLock());
2728
rwLock.DowngradeFromWriteLock();
2829
True(await rwLock.TryEnterReadLockAsync(DefaultTimeout));
2930
}
@@ -77,7 +78,7 @@ public static void OptimisticRead()
7778
True(rwLock.Validate(stamp));
7879
rwLock.Release();
7980
Equal(stamp, rwLock.TryOptimisticRead());
80-
True(rwLock.TryEnterWriteLock());
81+
True(rwLock.TryEnterWriteLock(stamp));
8182
False(rwLock.IsReadLockHeld);
8283
True(rwLock.IsWriteLockHeld);
8384
False(rwLock.Validate(stamp));
@@ -195,4 +196,64 @@ public static async Task LockStealing2()
195196
@lock.Release();
196197
await task3;
197198
}
199+
200+
[Fact]
201+
public static async Task DisposedWhenSynchronousReadLockAcquired()
202+
{
203+
var l = new AsyncReaderWriterLock();
204+
True(l.TryEnterReadLock());
205+
206+
var t = Task.Factory.StartNew(() => l.TryEnterWriteLock(DefaultTimeout), TaskCreationOptions.LongRunning);
207+
208+
l.Dispose();
209+
await ThrowsAsync<ObjectDisposedException>(Func.Constant(t));
210+
}
211+
212+
[Fact]
213+
public static async Task DisposedWhenSynchronousWriteLockAcquired()
214+
{
215+
var l = new AsyncReaderWriterLock();
216+
True(l.TryEnterWriteLock());
217+
218+
var t = Task.Factory.StartNew(() => l.TryEnterReadLock(DefaultTimeout), TaskCreationOptions.LongRunning);
219+
220+
l.Dispose();
221+
await ThrowsAsync<ObjectDisposedException>(Func.Constant(t));
222+
}
223+
224+
[Fact]
225+
public static async Task AcquireReadWriteLockSynchronously()
226+
{
227+
using var l = new AsyncReaderWriterLock();
228+
True(l.TryEnterReadLock(DefaultTimeout));
229+
True(l.TryEnterReadLock(DefaultTimeout));
230+
Equal(2L, l.CurrentReadCount);
231+
232+
var t = Task.Factory.StartNew(() => l.TryEnterWriteLock(DefaultTimeout), TaskCreationOptions.LongRunning);
233+
234+
l.Release();
235+
l.Release();
236+
237+
True(await t);
238+
True(l.IsWriteLockHeld);
239+
240+
l.Release();
241+
False(l.IsWriteLockHeld);
242+
}
243+
244+
[Fact]
245+
public static async Task ResumeMultipleReadersSynchronously()
246+
{
247+
using var l = new AsyncReaderWriterLock();
248+
True(l.TryEnterWriteLock());
249+
250+
var t1 = Task.Factory.StartNew(TryEnterReadLock, TaskCreationOptions.LongRunning);
251+
var t2 = Task.Factory.StartNew(TryEnterReadLock, TaskCreationOptions.LongRunning);
252+
253+
l.Release();
254+
Equal(new[] { true, true }, await Task.WhenAll(t1, t2));
255+
Equal(2L, l.CurrentReadCount);
256+
257+
bool TryEnterReadLock() => l.TryEnterReadLock(DefaultTimeout);
258+
}
198259
}

src/DotNext.Tests/Threading/AsyncResetEventTests.cs

+61
Original file line numberDiff line numberDiff line change
@@ -104,4 +104,65 @@ public static async Task RegressionIssue82()
104104
ev.Set();
105105
await consumer;
106106
}
107+
108+
public static TheoryData<IAsyncResetEvent> GetResetEvents() => new()
109+
{
110+
new AsyncAutoResetEvent(false),
111+
new AsyncManualResetEvent(false),
112+
};
113+
114+
[Theory]
115+
[MemberData(nameof(GetResetEvents))]
116+
public static async Task ManualResetEventSynchronousCompletion(IAsyncResetEvent resetEvent)
117+
{
118+
using (resetEvent)
119+
{
120+
False(resetEvent.IsSet);
121+
122+
var t = Task.Factory.StartNew(() => resetEvent.Wait(DefaultTimeout), TaskCreationOptions.LongRunning);
123+
124+
True(resetEvent.Signal());
125+
True(await t);
126+
Equal(resetEvent.ResetMode is EventResetMode.ManualReset, resetEvent.IsSet);
127+
}
128+
}
129+
130+
[Theory]
131+
[MemberData(nameof(GetResetEvents))]
132+
public static void AlreadySignaledEvents(IAsyncResetEvent resetEvent)
133+
{
134+
using (resetEvent)
135+
{
136+
True(resetEvent.Signal());
137+
True(resetEvent.Wait(DefaultTimeout));
138+
}
139+
}
140+
141+
[Fact]
142+
public static async Task AutoResetOnSyncWait()
143+
{
144+
using var are = new AsyncAutoResetEvent(false);
145+
var t = Task.Factory.StartNew(() => are.Wait(DefaultTimeout), TaskCreationOptions.LongRunning);
146+
True(are.Set());
147+
148+
True(await t);
149+
False(are.IsSet);
150+
}
151+
152+
[Fact]
153+
public static async Task ResumeSuspendedCallersSequentially()
154+
{
155+
using var are = new AsyncAutoResetEvent(false);
156+
var t1 = Task.Factory.StartNew(Wait, TaskCreationOptions.LongRunning);
157+
var t2 = Task.Factory.StartNew(Wait, TaskCreationOptions.LongRunning);
158+
159+
True(are.Set());
160+
161+
True(await Task.WhenAny(t1, t2).Unwrap());
162+
163+
True(are.Set());
164+
Equal(new[] { true, true }, await Task.WhenAll(t1, t2));
165+
166+
bool Wait() => are.Wait(DefaultTimeout);
167+
}
107168
}

src/DotNext.Threading/DotNext.Threading.csproj

+1-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
<ImplicitUsings>true</ImplicitUsings>
88
<IsTrimmable>true</IsTrimmable>
99
<Features>nullablePublicOnly</Features>
10-
<VersionPrefix>5.14.0</VersionPrefix>
10+
<VersionPrefix>5.15.0</VersionPrefix>
1111
<VersionSuffix></VersionSuffix>
1212
<Authors>.NET Foundation and Contributors</Authors>
1313
<Product>.NEXT Family of Libraries</Product>

src/DotNext.Threading/Runtime/Caching/RandomAccessCache.Dictionary.cs

+15-10
Original file line numberDiff line numberDiff line change
@@ -159,27 +159,32 @@ internal required TValue Value
159159
[DebuggerDisplay($"NumberOfItems = {{{nameof(Count)}}}")]
160160
internal sealed class Bucket : AsyncExclusiveLock
161161
{
162+
private bool newPairAdded;
162163
private volatile KeyValuePair? first; // volatile
163164

164165
[ExcludeFromCodeCoverage]
165166
private (int Alive, int Dead) Count => first?.BucketNodesCount ?? default;
166167

167-
internal KeyValuePair? TryAdd(IEqualityComparer<TKey>? keyComparer, TKey key, int hashCode, TValue value)
168+
internal KeyValuePair? TryAdd(TKey key, int hashCode, TValue value)
168169
{
169-
var firstCopy = first;
170-
if (firstCopy is not null && firstCopy.KeyHashCode == hashCode
171-
&& (keyComparer?.Equals(key, firstCopy.Key)
172-
?? EqualityComparer<TKey>.Default.Equals(key, firstCopy.Key)))
170+
KeyValuePair? result;
171+
if (newPairAdded)
173172
{
174-
return null;
173+
result = null;
174+
}
175+
else
176+
{
177+
result = CreatePair(key, value, hashCode);
178+
result.NextInBucket = first;
179+
first = result;
180+
newPairAdded = true;
175181
}
176182

177-
var newPair = CreatePair(key, value, hashCode);
178-
newPair.NextInBucket = firstCopy;
179-
first = newPair;
180-
return newPair;
183+
return result;
181184
}
182185

186+
internal void MarkAsReadyToAdd() => newPairAdded = false;
187+
183188
private void Remove(KeyValuePair? previous, KeyValuePair current)
184189
{
185190
ref var location = ref previous is null ? ref first : ref previous.NextInBucket;

0 commit comments

Comments
 (0)