.NET 中的Mutex类有助于管理对资源的独占访问。当给定一个名字时,这甚至可以跨进程完成,这非常方便。
不过,如果您曾经使用过 a Mutex
,您可能会发现它不能与async
/结合使用await
。更具体地说,来自文档:
互斥量具有线程亲和性;也就是说,互斥量只能由拥有它的线程释放。
这有时会使Mutex
该类难以使用,并且可能需要使用像GetAwaiter().GetResult()
.
对于进程内同步,SemaphoreSlim可以是一个不错的选择,因为它有一个WaitAsync()方法。然而,信号量不是管理独占访问的理想选择(new SemaphoreSlim(1)
有效但不太清楚)并且不支持系统范围的同步,例如。new Mutex(initiallyOwned: false, @"Global\MyMutex")
.
下面我将解释如何实现异步互斥锁,但完整的代码可以在底部或Gist中找到。
编辑根据大量反馈,我很清楚我对这篇文章的概括过度了。此实现专门用于跨进程同步,而不是进程内同步。下面的代码绝对不是线程安全的。因此,将其更多地视为“异步全局互斥锁”并坚持SemaphoreSlim
跨线程同步。
如何使用互斥锁
首先,介绍一些有关如何正确使用Mutex
. 最简单的例子是:
// Create the named system-wide mutex
using Mutex mutex = new(false, @"Global\MyMutex");
// Acquire the Mutex
mutex.WaitOne();
// Do work...
// Release the Mutex
mutex.ReleaseMutex();
Mutex
源自WaitHandle,WaitOne()
是获取它的机制。
但是,如果在Mutex
持有 a 的线程退出时 a 未正确释放,WaitOne()
则将抛出 a AbandonedMutexException。其原因解释如下:
废弃的互斥量通常表示代码中存在严重错误。当一个线程在没有释放互斥量的情况下退出时,受互斥量保护的数据结构可能不会处于一致的状态。如果可以验证数据结构的完整性,下一个请求互斥锁所有权的线程可以处理此异常并继续。
因此,下一个获取数据的线程Mutex
负责验证数据完整性(如果适用)。请注意,Mutex
如果用户终止进程,线程可以在未正确释放的情况下退出,因此AbandonedMutexException
在尝试获取Mutex
.
有了这个,我们的新例子就变成了:
// Create the named system-wide mutex
using Mutex mutex = new(false, @"Global\MyMutex");
try
{
// Acquire the Mutex
mutex.WaitOne();
}
catch (AbandonedMutexException)
{
// Abandoned by another process, we acquired it.
}
// Do work...
// Release the Mutex
mutex.ReleaseMutex();
但是,如果我们在持有时想要做的工作Mutex
是异步的怎么办?
AsyncMutex
首先让我们定义我们想要的类的形状。我们希望能够异步获取和释放互斥量,因此下面的代码似乎是合理的:
public sealed class AsyncMutex : IAsyncDisposable
{
public AsyncMutex(string name);
public Task AcquireAsync(CancellationToken cancellationToken);
public Task ReleaseAsync();
public ValueTask DisposeAsync();
}
因此,预期用途如下:
// Create the named system-wide mutex
await using AsyncMutex mutex = new(@"Global\MyMutex");
// Acquire the Mutex
await mutex.AcquireAsync(cancellationToken);
// Do async work...
// Release the Mutex
await mutex.ReleaseAsync();
既然我们知道我们想要它是什么样子,我们就可以开始实施了。
获取
因为Mutex
必须在单线程中,并且因为我们想要返回一个Task
所以互斥量可以异步获取,我们可以启动一个新Task
的使用Mutex
并返回它。
public Task AcquireAsync()
{
TaskCompletionSource taskCompletionSource = new();
// Putting all mutex manipulation in its own task as it doesn't work in async contexts
// Note: this task should not throw.
Task.Factory.StartNew(
state =>
{
try
{
using var mutex = new Mutex(false, _name);
try
{
// Acquire the Mutex
mutex.WaitOne();
}
catch (AbandonedMutexException)
{
// Abandoned by another process, we acquired it.
}
taskCompletionSource.SetResult();
// TODO: We need to release the mutex at some point
}
catch (Exception ex)
{
taskCompletionSource.TrySetException(ex);
}
}
state: null,
cancellationToken,
TaskCreationOptions.LongRunning,
TaskScheduler.Default);
return taskCompletionSource.Task;
}
所以现在AcquireAsync
返回 aTask
直到获得它才完成Mutex
。
释放
在某些时候,代码需要释放Mutex
. Task
因为互斥量必须在获取它的同一个线程中释放,所以它必须在AcquireAsync
启动的线程中释放。但是,我们不想在ReleaseAsync
被调用之前真正释放互斥量,所以我们需要Task
等到那个时候。
为此,我们需要一个ManualResetEventSlim
可以Task
等待信号的信号,ReleaseAsync
它将设置。
private Task? _mutexTask;
private ManualResetEventSlim? _releaseEvent;
public Task AcquireAsync(CancellationToken cancellationToken)
{
TaskCompletionSource taskCompletionSource = new();
_releaseEvent = new ManualResetEventSlim();
// Putting all mutex manipulation in its own task as it doesn't work in async contexts
// Note: this task should not throw.
_mutexTask = Task.Factory.StartNew(
state =>
{
try
{
using var mutex = new Mutex(false, _name);
try
{
// Acquire the Mutex
mutex.WaitOne();
}
catch (AbandonedMutexException)
{
// Abandoned by another process, we acquired it.
}
taskCompletionSource.SetResult();
// Wait until the release call
_releaseEvent.Wait();
mutex.ReleaseMutex();
}
catch (Exception ex)
{
taskCompletionSource.TrySetException(ex);
}
},
state: null,
cancellationToken,
TaskCreationOptions.LongRunning,
TaskScheduler.Default);
return taskCompletionSource.Task;
}
public async Task ReleaseAsync()
{
_releaseEvent?.Set();
if (_mutexTask != null)
{
await _mutexTask;
}
}
现在Task
将获取Mutex
,然后等待来自该ReleaseAsync
方法的信号以释放互斥量。
此外,ReleaseAsync
等待Task
完成以确保Task
在释放互斥锁之前不会完成。
消除
调用者可能不想永远等待互斥锁的获取,所以我们需要取消支持。这非常简单,因为Mutex
是WaitHandle
, 并且CancellationToken有一个WaitHandle属性,所以我们可以使用WaitHandle.WaitAny()
public Task AcquireAsync(CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
TaskCompletionSource taskCompletionSource = new();
_releaseEvent = new ManualResetEventSlim();
// Putting all mutex manipulation in its own task as it doesn't work in async contexts
// Note: this task should not throw.
_mutexTask = Task.Factory.StartNew(
state =>
{
try
{
using var mutex = new Mutex(false, _name);
try
{
// Wait for either the mutex to be acquired, or cancellation
if (WaitHandle.WaitAny(new[] { mutex, cancellationToken.WaitHandle }) != 0)
{
taskCompletionSource.SetCanceled(cancellationToken);
return;
}
}
catch (AbandonedMutexException)
{
// Abandoned by another process, we acquired it.
}
taskCompletionSource.SetResult();
// Wait until the release call
_releaseEvent.Wait();
mutex.ReleaseMutex();
}
catch (OperationCanceledException)
{
taskCompletionSource.TrySetCanceled(cancellationToken);
}
catch (Exception ex)
{
taskCompletionSource.TrySetException(ex);
}
},
state: null,
cancellationToken,
TaskCreationOptions.LongRunning,
TaskScheduler.Default);
return taskCompletionSource.Task;
}
处理
为确保释放互斥锁,我们应该实施处置。如果持有,这应该释放互斥锁。它还应该取消任何当前正在等待的互斥量获取,这需要链接的取消令牌。
private CancellationTokenSource? _cancellationTokenSource;
public Task AcquireAsync(CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
TaskCompletionSource taskCompletionSource = new();
_releaseEvent = new ManualResetEventSlim();
_cancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
// Putting all mutex manipulation in its own task as it doesn't work in async contexts
// Note: this task should not throw.
_mutexTask = Task.Factory.StartNew(
state =>
{
try
{
CancellationToken cancellationToken = _cancellationTokenSource.Token;
using var mutex = new Mutex(false, _name);
try
{
// Wait for either the mutex to be acquired, or cancellation
if (WaitHandle.WaitAny(new[] { mutex, cancellationToken.WaitHandle }) != 0)
{
taskCompletionSource.SetCanceled(cancellationToken);
return;
}
}
catch (AbandonedMutexException)
{
// Abandoned by another process, we acquired it.
}
taskCompletionSource.SetResult();
// Wait until the release call
_releaseEvent.Wait();
mutex.ReleaseMutex();
}
catch (OperationCanceledException)
{
taskCompletionSource.TrySetCanceled(cancellationToken);
}
catch (Exception ex)
{
taskCompletionSource.TrySetException(ex);
}
},
state: null,
cancellationToken,
TaskCreationOptions.LongRunning,
TaskScheduler.Default);
return taskCompletionSource.Task;
}
public async ValueTask DisposeAsync()
{
// Ensure the mutex task stops waiting for any acquire
_cancellationTokenSource?.Cancel();
// Ensure the mutex is released
await ReleaseAsync();
_releaseEvent?.Dispose();
_cancellationTokenSource?.Dispose();
}
结论
AsyncMutex
允许使用Mutex
with async
/ await
。
将整个事情放在一起(或查看要点):
public sealed class AsyncMutex : IAsyncDisposable
{
private readonly string _name;
private Task? _mutexTask;
private ManualResetEventSlim? _releaseEvent;
private CancellationTokenSource? _cancellationTokenSource;
public AsyncMutex(string name)
{
_name = name;
}
public Task AcquireAsync(CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
TaskCompletionSource taskCompletionSource = new();
_releaseEvent = new ManualResetEventSlim();
_cancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
// Putting all mutex manipulation in its own task as it doesn't work in async contexts
// Note: this task should not throw.
_mutexTask = Task.Factory.StartNew(
state =>
{
try
{
CancellationToken cancellationToken = _cancellationTokenSource.Token;
using var mutex = new Mutex(false, _name);
try
{
// Wait for either the mutex to be acquired, or cancellation
if (WaitHandle.WaitAny(new[] { mutex, cancellationToken.WaitHandle }) != 0)
{
taskCompletionSource.SetCanceled(cancellationToken);
return;
}
}
catch (AbandonedMutexException)
{
// Abandoned by another process, we acquired it.
}
taskCompletionSource.SetResult();
// Wait until the release call
_releaseEvent.Wait();
mutex.ReleaseMutex();
}
catch (OperationCanceledException)
{
taskCompletionSource.TrySetCanceled(cancellationToken);
}
catch (Exception ex)
{
taskCompletionSource.TrySetException(ex);
}
},
state: null,
cancellationToken,
TaskCreationOptions.LongRunning,
TaskScheduler.Default);
return taskCompletionSource.Task;
}
public async Task ReleaseAsync()
{
_releaseEvent?.Set();
if (_mutexTask != null)
{
await _mutexTask;
}
}
public async ValueTask DisposeAsync()
{
// Ensure the mutex task stops waiting for any acquire
_cancellationTokenSource?.Cancel();
// Ensure the mutex is released
await ReleaseAsync();
_releaseEvent?.Dispose();
_cancellationTokenSource?.Dispose();
}
}