• C#中Lock和await的用法
  • 发布于 2个月前
  • 432 热度
    0 评论
如果试图在 lock 块中使用 async 关键字时使用 lock 关键字,会得到这个编译错误:cannot await in the body of a lock statement。原因是在async 完成之后,该方法可能会在一个不同的线程中运行,而不是在async 关键字之前。lock 关键字需要同一个线程中获取锁和释放锁。

下面的代码块会导致编译错误:
static async Task IncorrectLockAsync()
{
  lock (s_syncLock)
  {
    Console.WriteLine($"{nameof(IncorrectLockAsync)} started");
    await Task.Delay(500);  // compiler error: cannot await in the body
      // of a lock statement
    Console.WriteLine($"{nameof(IncorrectLockAsync)) ending");
  }
}
如何解决这个问题?不能为此使用 Monitor,因为 Monitor 需要从它获取锁的同一线程中释放锁。lock 关键字基于 Monitor。

虽然 Mutex 对象可以用于不同进程之间的同步,但它有相同的问题:它为线程授予了一个锁。从不同的线程中释放锁是不可能的。相反,可以使用 Semaphore 或 SemaphoreSlim 类。Semaphore 可以从不同的线程中释放信号量。

下面的代码片段使用 SemaphoreSlim 对象上的 WaitAsync 等待获得一个信号量。SemaphoreSlim 对象初始化为计数 1,因此对信号量的等待只授予一次。在finally代码块中,通过调用Release方法释放信号量:
private static SemaphoreSlim s_asyncLock = new SemaphoreSlim(1); 
static async Task LockWithSemaphore(string title)
{
  Console.WriteLine($"{title} waiting for lock"); 
  await s_asyncLock.WaitAsync(); 
  try
  {
    Console.WriteLine($"{title} {nameof(LockWithSemaphore)} started"); 
    await Task.Delay(500);
    Console.WriteLine($"{title} {nameof(LockWithSemaphore)} ending");
  }
  finally
  {
    s_asyncLock.Release();
  }
}
下面尝试在多个任务中同时调用此方法。该方法 RunUseSemaphoreAsync 启动6 个任务,并发地调用 LockWithSemaphore 方法:
static async Task RunUseSemaphoreAsync()
{
  Console.WriteLine(nameof(RunUseSemaphoreAsync));
  string[] messages = ( "one", "two", "three", "four", "five", "six" }; 
  Task[] tasks = new Task[messages.Length];
  for (int i = 0; i < messages.Length; i++)
  {
    string message = messages[i];
      
    tasks[i] = Task.Run(async() =>
    {
      await LockWithSemaphore(message);
    }) ;
  }
    
  await Task.WhenAll(tasks); 
  Console.WriteLine();
}
运行该程序,可以看到多个任务同时启动,但是在信号量被锁定后,所有其他任务都需要等待信号量再次释放:
RunLockWithAwaitAsync 
two waiting for lock
two LockWithSemaphore started 
three waiting for lock 
five waiting for lock 
four waiting for lock 
six waiting for lock 
one waiting for lock
two LockWithSemaphore ending
three LockWithSemaphore started 
three LockWithSemaphore ending 
five LockWithSemaphore started 
five LockWithSemaphore ending 
four LockWithSemaphore started 
four LockWithSemaphore ending 
six LockWithSemaphore started 
six LockWithSemaphore ending
one LockWithSemaphore started 
one LockWithSemaphore ending
为了更容易地使用锁,可以创建一个实现 IDisposable 接口的类来管理资源。对于这个类。可以使用 using 语句,就像使用 lock 状态来锁定和释放信号量一样。

下面的代码片段实现了 AsyncSemaphore 类,该类在构造函数中分配一个SemaphoreSlim,在 AsyncSemaphore 上调用 WaitAsync 方法时,返回实现接口 IDisposable 的内部类 
SemaphoreReleaser。调用 Dispose 方法时,释放信号量:
public sealed class AsyncSemaphore
{
  private class SemaphoreReleaser : IDisposable
  {
    private SemaphoreSlim _semaphore;
    
    public SemaphoreReleaser(SemaphoreSlim semaphore) => 
      _semaphore = semaphore;
      
    public void Dispose() => _semaphore.Release();
  }

  private SemaphoreSlim _semaphore; 
  public AsyncSemaphore() =>
    _semaphore = new SemaphoreSlim(1);
    
  public async Task<IDisposable> WaitAsync()
  {
    await _semaphore.WaitAsync();
    return new SemaphoreReleaser(_semaphore) as IDisposable;
  }
}
从前面所示的 LockWithSemaphore 方法中更改实现,现在可以使用 using 语句锁定信号量。记住,using 语句创建一个 catch/finally 块,在 finally 块中调用 Dispose 方法:
private static AsyncSemaphore s_asyncSemaphore = new AsyncSemaphore(); 
static async Task UseAsyncSemaphore(string title)
{
  using (await s_asyncSemaphore.WaitAsync())
  {
    Console.WriteLine($"{title}  {nameof(LockWithSemaphore)} started"); 
    await Task.Delay(500);
    Console.WriteLine($"{title} {nameof(LockWithSemaphore)} ending");
  }
}

使用类似于 LockWithSemaphore 方法的 UseAsyncSemaphore 方法会执行相同的行为。然而,类只编写一次,等待过程中的锁定就变得更简单。

用户评论