Rework LockGuard to avoid allocations when the lock isn't contested

This commit is contained in:
js6pak committed 2023-12-17 23:29:31 +01:00
1 parent d91ab5cfe8
commit 43acae16ed
4 files changed
+79 -69

No files matched your search

+4 -3
View File
@@ -6,6 +6,7 @@ using Impostor.Api.Innersloth;
using Impostor.Api.Innersloth.GameOptions;
using Impostor.Api.Net;
using Impostor.Api.Net.Inner;
using Impostor.Api.Utils;
namespace Impostor.Api.Games
{
@@ -53,10 +54,10 @@ namespace Impostor.Api.Games
T? FindObjectByNetId<T>(uint netId)
where T : IInnerNetObject;
/// <summary>Lock an IGame to prevent concurrent modification.</summary>
/// <returns>A <see cref="LockGuard" /> that should be awaited before and disposed after modifying the game.</returns>
/// <summary>Lock an <see cref="IGame"/> to prevent concurrent modification.</summary>
/// <returns>A <see cref="AsyncLock.Releaser" /> that should be disposed after modifying the game.</returns>
/// <remarks>This method should be called when modifying a game outside of an <see cref="IGameEvent" /> handler.</remarks>
Task<LockGuard> LockAsync();
ValueTask<AsyncLock.Releaser> LockAsync();
/// <summary>
/// Adds an <see cref="IPAddress" /> to the ban list of this game.
-61
View File
@@ -1,61 +0,0 @@
using System;
using System.Threading;
using System.Threading.Tasks;
namespace Impostor.Api.Net
{
/// <summary>RAII-style lock guard for use in Impostor.</summary>
/// <remarks>
/// To lock a semaphore, use the LockAsync method and await the result.
/// To unlock the semaphore, dispose of the LockGuard.
/// </remarks>
public sealed class LockGuard : IDisposable
{
private readonly SemaphoreSlim _semaphore;
private bool _disposedValue;
private LockGuard(SemaphoreSlim semaphore)
{
_semaphore = semaphore;
}
private void Dispose(bool disposing)
{
if (!_disposedValue)
{
if (disposing)
{
_semaphore.Release();
}
_disposedValue = true;
}
}
/// <summary>Create a RAII-style lock guard for an instance of <see cref="SemaphoreSlim"/>.</summary>
/// <param name="semaphore">The semaphore to Wait and Release on.</param>
/// <param name="timeout">The amount of milliseconds to wait for the lock becoming available.</param>
/// <exception cref="ImpostorException">When the timeout expires.</exception>
/// <returns>The lock guard if the semaphore could be locked succesfully.</returns>
public static async Task<LockGuard> LockAsync(SemaphoreSlim semaphore, int timeout = 1000)
{
var success = await semaphore.WaitAsync(timeout);
if (success)
{
return new LockGuard(semaphore);
}
else
{
throw new ImpostorException("Could not lock semaphore");
}
}
/// <inheritdoc/>
public void Dispose()
{
Dispose(disposing: true);
GC.SuppressFinalize(this);
}
}
}
+71
View File
@@ -0,0 +1,71 @@
using System;
using System.Threading;
using System.Threading.Tasks;
namespace Impostor.Api.Utils;
/// <summary>
/// Represents an asynchronous lock mechanism.
/// </summary>
public sealed class AsyncLock
{
private readonly SemaphoreSlim _semaphore = new(1, 1);
/// <summary>
/// Asynchronously acquires a lock.
/// </summary>
/// <returns>
/// A <see cref="ValueTask{Releaser}"/> that represents the asynchronous lock operation.
/// The result of the task is a <see cref="Releaser"/> that should be disposed to release the lock.
/// </returns>
public ValueTask<Releaser> LockAsync()
{
return LockAsync(CancellationToken.None);
}
/// <inheritdoc cref="LockAsync()"/>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to observe while waiting for the lock.</param>
/// <exception cref="T:System.OperationCanceledException"><paramref name="cancellationToken" /> was canceled.</exception>
public ValueTask<Releaser> LockAsync(CancellationToken cancellationToken)
{
var wait = _semaphore.WaitAsync(cancellationToken);
if (wait.IsCompleted)
{
return ValueTask.FromResult(new Releaser(this));
}
return new ValueTask<Releaser>(wait.ContinueWith(
static (_, state) => new Releaser((AsyncLock)state!),
this,
cancellationToken,
TaskContinuationOptions.ExecuteSynchronously,
TaskScheduler.Default
));
}
/// <summary>
/// Represents a disposable object that, when disposed, releases the associated lock.
/// </summary>
public struct Releaser : IDisposable
{
private AsyncLock? _lock;
internal Releaser(AsyncLock @lock)
{
_lock = @lock;
}
/// <summary>
/// Releases the lock held by this <see cref="Releaser"/> instance.
/// </summary>
public void Dispose()
{
if (_lock != null)
{
_lock._semaphore.Release();
_lock = null;
}
}
}
}
+4 -5
View File
@@ -4,7 +4,6 @@ using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Net;
using System.Threading;
using System.Threading.Tasks;
using Impostor.Api.Config;
using Impostor.Api.Events.Managers;
@@ -14,6 +13,7 @@ using Impostor.Api.Innersloth.GameOptions;
using Impostor.Api.Net;
using Impostor.Api.Net.Manager;
using Impostor.Api.Net.Messages.S2C;
using Impostor.Api.Utils;
using Impostor.Server.Events;
using Impostor.Server.Net.Manager;
using Microsoft.Extensions.Logging;
@@ -33,7 +33,7 @@ namespace Impostor.Server.Net.State
private readonly ICompatibilityManager _compatibilityManager;
private readonly CompatibilityConfig _compatibilityConfig;
private readonly TimeoutConfig _timeoutConfig;
private readonly SemaphoreSlim _lock;
private readonly AsyncLock _lock = new();
public Game(
ILogger<Game> logger,
@@ -68,7 +68,6 @@ namespace Impostor.Server.Net.State
_compatibilityConfig = compatibilityConfig.Value;
_timeoutConfig = timeoutConfig.Value;
Items = new ConcurrentDictionary<object, object>();
_lock = new SemaphoreSlim(1, 1);
}
public IPEndPoint PublicIp { get; }
@@ -116,9 +115,9 @@ namespace Impostor.Server.Net.State
return _players.TryGetValue(clientId, out var clientPlayer) ? clientPlayer : null;
}
public async Task<LockGuard> LockAsync()
public ValueTask<AsyncLock.Releaser> LockAsync()
{
return await LockGuard.LockAsync(_lock);
return _lock.LockAsync();
}
internal async ValueTask StartedAsync()