现在看来,在异步代码中,SemaphoreSlim是lock(obj) {}的推荐替代品。我找到了关于如何使用它的建议:https://blog.cdemi.io/async-waiting-inside-c-sharp-locks/
特别是,这个人建议这样做:
//Instantiate a Singleton of the Semaphore with a value of 1. This means that only 1 thread can be granted access at a time.
static SemaphoreSlim semaphoreSlim = new SemaphoreSlim(1,1);//Asynchronously wait to enter the Semaphore. If no-one has been granted access to the Semaphore, code execution will proceed, otherwise this thread waits here until the semaphore is released
await semaphoreSlim.WaitAsync();
try
{
await Task.Delay(1000);
}
finally
{
//When the task is ready, release the semaphore. It is vital to ALWAYS release the semaphore when we are ready, or else we will end up with a Semaphore that is forever locked.
//This is why it is important to do the Release within a try...finally clause; program execution may crash or take a different path, this way you are guaranteed execution
semaphoreSlim.Release();
}在我看来,这段代码似乎违反了我以前看到的关于如何锁定的建议,这就是要记住,您的代码在任何时候都可能被中断,并为此编写代码。如果在await sempahoreSlim.WaitAsync()之后和输入try语句之前抛出任何异常,则永远不会释放信号量。这就是为什么我认为锁语句和the语句的引入带来了如此巨大的结果。
有没有什么地方的引用明确地解释了这段代码是有效的?也许在代码被中断之前就输入了try/finally语句,这是我以前从未听说过的吗?或者,是否有一种不同的模式,实际上是正确地使用信号量作为锁,或者异步.NET代码的其他锁定机制?
发布于 2020-06-25 14:21:21
是的,从理论上讲,await semaphoreSlim.WaitAsync();和try之间可能会发生一些事情,但在现实中:在这种情况下,你的应用程序已经面目全非了,因为它正处于呼叫栈内爆的过程中。在现实中,虽然这是一个理论上的关注,但无论如何,你没有什么可以做的,而且你的过程将像病态的事情一样被不体面地放下。
所以我的建议是:不要太担心
(在现实中,更大的风险是线程池螺旋死亡,这通常是由线程池线程上的同步过异步造成的,这意味着即使您在语义上获得了信号量,也没有池线程可以让您实际做这件事,从而允许您重新释放它)
发布于 2022-04-11 10:21:27
是的,你的假设是正确的。如果在输入try之前抛出异常,则永远不会释放SemaphoreSlim。虽然这是一个非常罕见的事件,但我认为在大多数情况下都是可以忽略的。例如,当代码作为一个任务执行时,任务可以被取消,不幸的是,它恰好发生在Wait()完成之后和try{}输入之前被取消。这是可能的,但不太可能,你不必担心。
作为示例提供的代码是有效的,甚至在Microsoft (https://learn.microsoft.com/en-us/dotnet/api/system.threading.semaphoreslim?view=net-6.0#examples)中也提供了类似的代码。
但是,如果您的程序在任何情况下都不能发生此事件,则可以使用must (){}实现lock(){}模式。这是我在所有项目中使用的类,而不是手动处理SemaphoreSlim或lock(){}语句。
public class MySemaphore : IDisposable
{
private string semaphoreLockKey;
private static Dictionary<string, SemaphoreSlim> internalSemaphoreSlimDict = new Dictionary<string, SemaphoreSlim>();
/// <summary>
/// <para>Creates a <see cref="MySemaphore"/> for the given <paramref name="key"/> and aquires the lock this <see cref="MySemaphore"/> represents.</para>
/// <para>The task this method returns will await the lock for this <see cref="MySemaphore"/> if the semaphore with the key is already in use.
/// Once the task aquired the lock, an instance of <see cref="MySemaphore"/> is returned, which will release the lock once <see cref="Dispose"/> is called (preferably via a using() statement)</para>
/// </summary>
/// <param name="key"></param>
/// <returns>Returns a <see cref="MySemaphore"/> that holds the lock of the given <paramref name="key"/>. Dispose the returned instance to release the lock (preferably via a using() statement)</returns>
/// <remarks>Wrap this into a using() to release the semaphore upon finishing your locked code</remarks>
public static async Task<MySemaphore> WaitForLockAsync(string key)
{
var mySemaphore = new MySemaphore(key);
await internalSemaphoreSlimDict[key].WaitAsync();
return mySemaphore;
}
/// <summary>
/// <para>Creates a <see cref="MySemaphore"/> for the given <paramref name="key"/> and aquires the lock this <see cref="MySemaphore"/> represents.</para>
/// <para>The task this method returns will await the lock for this <see cref="MySemaphore"/> if the semaphore with the key is already in use.
/// Once the task aquired the lock, an instance of <see cref="MySemaphore"/> is returned, which will release the lock once <see cref="Dispose"/> is called (preferably via a using() statement)</para>
/// </summary>
/// <param name="key"></param>
/// <returns>Returns a <see cref="MySemaphore"/> that holds the lock of the given <paramref name="key"/>. Dispose the returned instance to release the lock (preferably via a using() statement)</returns>
/// <remarks>Wrap this into a using() to release the semaphore upon finishing your locked code</remarks>
public static MySemaphore WaitForLock(string key)
{
var mySemaphore = new MySemaphore(key);
internalSemaphoreSlimDict[key].Wait();
return mySemaphore;
}
/// <summary>
/// Constructor using a key. If a key already exists and is currently used, it will lock the calling thread until the other thread has disposed his MySemaphore
/// </summary>
/// <param name="key"></param>
private MySemaphore(string key)
{
this.semaphoreLockKey = key;
if (!internalSemaphoreSlimDict.ContainsKey(key))
internalSemaphoreSlimDict[key] = new SemaphoreSlim(1, 1);
}
/// <summary>
/// Releases the Lock that is held by this instance
/// </summary>
public void Dispose()
{
internalSemaphoreSlimDict[semaphoreLockKey].Release();
}
}使用这个类,我们可以定义一个(字符串)键,为其创建一个SemaphoreSlim。两个互不了解的任务可以使用相同的键,从而等待其他任务完成。当我们调用MySemaphore.WaitForLock时,将创建一个新的MySemaphore实例(表示SemaphoreSlim),并调用SemaphoreSlim.Wait()。一旦获得了锁,就会返回MySemaphore实例。
由于我们将其包装为the (){}语句,因此每当退出using (通过完成代码或在进入using块后抛出任何异常)时,都会调用MySemaphore.Dispose(),从而释放锁。
那么,使用情况将是:
using (MySemaphore.WaitForLock("someLockKey"))
{
//do something
}或
using (await MySemaphore.WaitForLockAsync("someLockKey"))
{
await Task.Delay(1000);
}请注意,这样您就可以创建一个锁(){}样模式,该模式支持异步方法中的使用,并减少代码中的行数。根据Microsoft,在较早的C#版本中,using()语句被编译为实际上与使用try/finally相同。这意味着,只有在输入了using块之后才会释放MySemaphore --而不是当WaitForLock()仍然被执行时。这意味着,您仍然存在相同的问题,即在Wait()查询锁和正在输入的using块之间,SemaphoreSlim不会被释放。
但是,在使用using声明(参见C# )时,这一点在C# 8.0中已经发生了变化,如下所示:https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/using-statement
--新的使用语句语法的代码转换为类似的代码。try块在声明变量的位置打开。最后块在封闭块的结束处添加,通常在方法的末尾。
因为我们在Wait()调用之前声明了MySemaphore (当使用using声明时,而不是using语句),所以在任何情况下都会释放SemaphoreSlim。但是,您必须处理这样一种情况,即在Wait()能够对锁进行访问之前,可以调用Dispose()。这可以通过在MySemaphore类中添加一个新的布尔字段并在调用SemaphoreSlim.Wait()之后将其翻转为true来解决,并且只有在字段为true时才调用Release()。
private bool hasLock;
public void Dispose()
{
if(hasLock)
internalSemaphoreSlimDict[semaphoreLockKey].Release();
}但是,您需要更改MySemaphore的用法:
using MySemaphore ms = MySemaphore.WaitForLock("someLockKey");
//do something
//MySemaphore "ms" will be disposed when the current scope is exited或
using MySemaphore ms = await MySemaphore.WaitForLockAsync("someLockKey");
//do something
//MySemaphore "ms" will be disposed when the current scope is exited在这种情况下,一旦声明了"ms“实例,无论发生什么情况,所需的锁都会被保证释放。
https://stackoverflow.com/questions/62577492
复制相似问题