Skip to content

Commit 5f5f7bd

Browse files
committed
Port synchronization library from RecRoom codebase
1 parent 5cb589c commit 5f5f7bd

File tree

5 files changed

+663
-0
lines changed

5 files changed

+663
-0
lines changed
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
// Copyright (c) Six Labors.
2+
// Licensed under the Apache License, Version 2.0.
3+
4+
using System;
5+
using System.Collections.Concurrent;
6+
using System.Threading.Tasks;
7+
8+
#nullable enable
9+
10+
namespace SixLabors.ImageSharp.Web.Synchronization
11+
{
12+
/// <summary>
13+
/// Extension of the <see cref="AsyncLock"/> that enables fine-grained locking on a given key.
14+
/// Concurrent lock requests using different keys can execute simultaneously, while requests to lock
15+
/// using the same key will be forced to wait. This object is thread-safe and internally uses a pooling
16+
/// mechanism to minimize allocation of new locks.
17+
/// </summary>
18+
/// <typeparam name="TKey">The type of the key.</typeparam>
19+
public class AsyncKeyLock<TKey> : IDisposable
20+
where TKey : notnull
21+
{
22+
private readonly RefCountedConcurrentDictionary<TKey, AsyncLock> activeLocks;
23+
private readonly ConcurrentBag<AsyncLock> pool;
24+
private readonly int maxPoolSize;
25+
26+
/// <summary>
27+
/// Initializes a new instance of the <see cref="AsyncKeyLock{TKey}"/> class.
28+
/// </summary>
29+
/// <param name="maxPoolSize">The maximum number of locks that should be pooled for reuse.</param>
30+
public AsyncKeyLock(int maxPoolSize = 64)
31+
{
32+
this.pool = new ConcurrentBag<AsyncLock>();
33+
this.activeLocks = new RefCountedConcurrentDictionary<TKey, AsyncLock>(this.CreateLeasedLock, this.ReturnLeasedLock);
34+
this.maxPoolSize = maxPoolSize;
35+
}
36+
37+
/// <summary>
38+
/// Locks the current thread asynchronously.
39+
/// </summary>
40+
/// <param name="key">The key identifying the specific object to lock against.</param>
41+
/// <returns>
42+
/// The <see cref="AsyncLock.Releaser"/> that will release the lock.
43+
/// </returns>
44+
public Task<AsyncLock.Releaser> LockAsync(TKey key)
45+
=> this.activeLocks.Get(key).LockAsync();
46+
47+
/// <summary>
48+
/// Releases all resources used by the current instance of the <see cref="AsyncKeyLock{TKey}"/> class.
49+
/// </summary>
50+
public void Dispose()
51+
{
52+
// Dispose of all the locks in the pool
53+
while (this.pool.TryTake(out AsyncLock? asyncLock))
54+
{
55+
asyncLock.Dispose();
56+
}
57+
58+
// activeLocks SHOULD be empty at this point so we don't need to try and clear it out.
59+
// If it's not empty, then this object is being disposed of prematurely!
60+
}
61+
62+
private AsyncLock CreateLeasedLock(TKey key)
63+
{
64+
if (!this.pool.TryTake(out AsyncLock? asyncLock))
65+
{
66+
asyncLock = new AsyncLock();
67+
}
68+
69+
asyncLock.OnRelease = () => this.activeLocks.Release(key);
70+
return asyncLock;
71+
}
72+
73+
private void ReturnLeasedLock(AsyncLock asyncLock)
74+
{
75+
if (this.pool.Count < this.maxPoolSize)
76+
{
77+
this.pool.Add(asyncLock);
78+
}
79+
else
80+
{
81+
asyncLock.Dispose();
82+
}
83+
}
84+
}
85+
}
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
// Copyright (c) Six Labors.
2+
// Licensed under the Apache License, Version 2.0.
3+
4+
using System.Collections.Concurrent;
5+
using System.Threading.Tasks;
6+
7+
#nullable enable
8+
9+
namespace SixLabors.ImageSharp.Web.Synchronization
10+
{
11+
/// <summary>
12+
/// Extension of the <see cref="AsyncReaderWriterLock"/> that enables fine-grained locking on a given key.
13+
/// Concurrent write lock requests using different keys can execute simultaneously, while requests to lock
14+
/// using the same key will be forced to wait. This object is thread-safe and internally uses a pooling
15+
/// mechanism to minimize allocation of new locks.
16+
/// </summary>
17+
/// <typeparam name="TKey">The type of the key.</typeparam>
18+
public class AsyncKeyReaderWriterLock<TKey>
19+
where TKey : notnull
20+
{
21+
private readonly RefCountedConcurrentDictionary<TKey, AsyncReaderWriterLock> activeLocks;
22+
private readonly ConcurrentBag<AsyncReaderWriterLock> pool;
23+
private readonly int maxPoolSize;
24+
25+
/// <summary>
26+
/// Initializes a new instance of the <see cref="AsyncKeyReaderWriterLock{TKey}"/> class.
27+
/// </summary>
28+
/// <param name="maxPoolSize">The maximum number of locks that should be pooled for reuse.</param>
29+
public AsyncKeyReaderWriterLock(int maxPoolSize = 64)
30+
{
31+
this.pool = new ConcurrentBag<AsyncReaderWriterLock>();
32+
this.activeLocks = new RefCountedConcurrentDictionary<TKey, AsyncReaderWriterLock>(this.CreateLeasedLock, this.ReturnLeasedLock);
33+
this.maxPoolSize = maxPoolSize;
34+
}
35+
36+
/// <summary>
37+
/// Locks the current thread in read mode asynchronously.
38+
/// </summary>
39+
/// <param name="key">The key identifying the specific object to lock against.</param>
40+
/// <returns>
41+
/// The <see cref="AsyncReaderWriterLock.Releaser"/> that will release the lock.
42+
/// </returns>
43+
public Task<AsyncReaderWriterLock.Releaser> ReaderLockAsync(TKey key)
44+
=> this.activeLocks.Get(key).ReaderLockAsync();
45+
46+
/// <summary>
47+
/// Locks the current thread in write mode asynchronously.
48+
/// </summary>
49+
/// <param name="key">The key identifying the specific object to lock against.</param>
50+
/// <returns>
51+
/// The <see cref="AsyncReaderWriterLock.Releaser"/> that will release the lock.
52+
/// </returns>
53+
public Task<AsyncReaderWriterLock.Releaser> WriterLockAsync(TKey key)
54+
=> this.activeLocks.Get(key).WriterLockAsync();
55+
56+
private AsyncReaderWriterLock CreateLeasedLock(TKey key)
57+
{
58+
if (!this.pool.TryTake(out AsyncReaderWriterLock? asyncLock))
59+
{
60+
asyncLock = new AsyncReaderWriterLock();
61+
}
62+
63+
asyncLock.OnRelease = () => this.activeLocks.Release(key);
64+
return asyncLock;
65+
}
66+
67+
private void ReturnLeasedLock(AsyncReaderWriterLock asyncLock)
68+
{
69+
if (this.pool.Count < this.maxPoolSize)
70+
{
71+
this.pool.Add(asyncLock);
72+
}
73+
}
74+
}
75+
}
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
// Copyright (c) Six Labors.
2+
// Licensed under the Apache License, Version 2.0.
3+
4+
using System;
5+
using System.Threading;
6+
using System.Threading.Tasks;
7+
8+
#nullable enable
9+
10+
namespace SixLabors.ImageSharp.Web.Synchronization
11+
{
12+
/// <summary>
13+
/// An asynchronous locker that uses an IDisposable pattern for releasing the lock.
14+
/// </summary>
15+
public class AsyncLock : IDisposable
16+
{
17+
private readonly SemaphoreSlim semaphore;
18+
#pragma warning disable IDE0069 // Disposable fields should be disposed
19+
private readonly Releaser releaser;
20+
#pragma warning restore IDE0069 // Disposable fields should be disposed
21+
private readonly Task<Releaser> releaserTask;
22+
23+
/// <summary>
24+
/// Initializes a new instance of the <see cref="AsyncLock"/> class.
25+
/// </summary>
26+
public AsyncLock()
27+
{
28+
this.semaphore = new SemaphoreSlim(1, 1);
29+
this.releaser = new Releaser(this);
30+
this.releaserTask = Task.FromResult(this.releaser);
31+
}
32+
33+
/// <summary>
34+
/// Gets or sets the callback that should be invoked whenever this lock is released.
35+
/// </summary>
36+
public Action? OnRelease { get; set; }
37+
38+
/// <summary>
39+
/// Asynchronously obtains the lock. Dispose the returned <see cref="Releaser"/> to release the lock.
40+
/// </summary>
41+
/// <returns>
42+
/// The <see cref="Releaser"/> that will release the lock.
43+
/// </returns>
44+
public Task<Releaser> LockAsync()
45+
{
46+
Task wait = this.semaphore.WaitAsync();
47+
48+
// No-allocation fast path when the semaphore wait completed synchronously
49+
return (wait.Status == TaskStatus.RanToCompletion)
50+
? this.releaserTask
51+
: AwaitThenReturn(wait, this.releaser);
52+
53+
static async Task<Releaser> AwaitThenReturn(Task t, Releaser r)
54+
{
55+
await t;
56+
return r;
57+
}
58+
}
59+
60+
private void Release()
61+
{
62+
try
63+
{
64+
this.semaphore.Release();
65+
}
66+
finally
67+
{
68+
this.OnRelease?.Invoke();
69+
}
70+
}
71+
72+
/// <summary>
73+
/// Releases all resources used by the current instance of the <see cref="AsyncLock"/> class.
74+
/// </summary>
75+
public void Dispose() => this.semaphore.Dispose();
76+
77+
/// <summary>
78+
/// Utility class that releases an <see cref="AsyncLock"/> on disposal.
79+
/// </summary>
80+
public sealed class Releaser : IDisposable
81+
{
82+
private readonly AsyncLock toRelease;
83+
84+
internal Releaser(AsyncLock toRelease) => this.toRelease = toRelease;
85+
86+
/// <summary>
87+
/// Releases the <see cref="AsyncLock"/> associated with this <see cref="Releaser"/>.
88+
/// </summary>
89+
public void Dispose() => this.toRelease?.Release();
90+
}
91+
}
92+
}

0 commit comments

Comments
 (0)