Books
Books copied to clipboard
.NET 9 Lock 类在异步方法中的错误用法及解决方案
.NET 9 Lock 类在异步方法中的错误用法及解决方案
问题描述
在使用 .NET 9 新引入的 Lock 类时,在异步方法中出现了状态不一致的问题。第一次执行正常,但多次调用后 Lock 的状态变得异常,导致后续调用无法正常工作。
错误的代码示例
public class BackgroundService
{
private static readonly Lock lockObject = new();
private readonly IRepository repository;
private readonly IFileProcessor fileProcessor;
private async ValueTask ProcessDataAsync()
{
if (lockObject.TryEnter())
{
try
{
// ❌ 错误:在 Lock 的 enter 和 exit 之间使用了 await
var items = await repository.GetPendingItemsAsync();
while (items.Count > 0)
{
foreach (var item in items)
{
var data = await fileProcessor.DownloadAsync(item.Url);
if (data.Length > 0)
{
var result = await fileProcessor.UploadAsync(data, item.Id);
await repository.UpdateItemAsync(item.Id, result);
}
}
items = await repository.GetPendingItemsAsync();
}
}
finally
{
lockObject.Exit(); // 可能在不同线程上执行,导致状态异常
}
}
}
}
根本原因
根据微软官方文档说明:
When the lock is being entered and exited in a C# async method, ensure that there is no await between the enter and exit. Locks are held by threads and the code following an await might run on a different thread.
问题出现的原因:
Lock类绑定到特定线程await操作可能导致代码在不同线程上恢复执行- 在线程 A 获取的锁,却在线程 B 上尝试释放,导致状态不一致
解决方案
方案1:使用 SemaphoreSlim(推荐)
public class BackgroundService
{
private static readonly SemaphoreSlim semaphore = new(1, 1);
private readonly IRepository repository;
private readonly IFileProcessor fileProcessor;
private async ValueTask ProcessDataAsync()
{
// SemaphoreSlim 专为异步设计,不绑定特定线程
if (!await semaphore.WaitAsync(0))
return; // 已有实例在运行
try
{
// ✅ 可以安全使用 await
var items = await repository.GetPendingItemsAsync();
while (items.Count > 0)
{
foreach (var item in items)
{
var data = await fileProcessor.DownloadAsync(item.Url);
if (data.Length > 0)
{
var result = await fileProcessor.UploadAsync(data, item.Id);
await repository.UpdateItemAsync(item.Id, result);
}
}
items = await repository.GetPendingItemsAsync();
}
}
finally
{
semaphore.Release();
}
}
}
方案2:使用原子操作
public class BackgroundService
{
private static int isRunning = 0;
private readonly IRepository repository;
private readonly IFileProcessor fileProcessor;
private async ValueTask ProcessDataAsync()
{
// 原子操作检查并设置运行状态
if (Interlocked.Exchange(ref isRunning, 1) == 1)
return; // 已有实例在运行
try
{
// ✅ 可以安全使用 await
var items = await repository.GetPendingItemsAsync();
while (items.Count > 0)
{
foreach (var item in items)
{
var data = await fileProcessor.DownloadAsync(item.Url);
if (data.Length > 0)
{
var result = await fileProcessor.UploadAsync(data, item.Id);
await repository.UpdateItemAsync(item.Id, result);
}
}
items = await repository.GetPendingItemsAsync();
}
}
finally
{
Interlocked.Exchange(ref isRunning, 0);
}
}
}
方案3:如果必须使用 Lock,避免 await
public class BackgroundService
{
private static readonly Lock lockObject = new();
private readonly IRepository repository;
private readonly IFileProcessor fileProcessor;
private async ValueTask ProcessDataAsync()
{
bool shouldProcess = false;
// 在锁内只做同步检查
using (var scope = lockObject.TryEnterScope())
{
if (scope.IsHeld)
{
shouldProcess = CheckIfProcessingNeeded(); // 同步操作
}
}
if (shouldProcess)
{
// 在锁外执行异步操作
await ProcessItemsAsync();
}
}
private bool CheckIfProcessingNeeded()
{
// 同步检查逻辑,不包含 await
return true;
}
private async Task ProcessItemsAsync()
{
// 实际的异步处理逻辑
var items = await repository.GetPendingItemsAsync();
// ... 处理逻辑
}
}
关键要点
- ✅ 正确做法:在异步方法中使用
SemaphoreSlim或原子操作 - ❌ 错误做法:在包含
await的异步方法中使用 .NET 9 的Lock类 - 📖 官方建议:Lock 的 enter 和 exit 之间不应该有
await操作 - 🔧 最佳实践:对于后台任务的单实例控制,推荐使用
SemaphoreSlim
测试验证
可以使用以下代码验证修复效果:
// 测试并发调用
var tasks = new List<Task>();
for (int i = 0; i < 10; i++)
{
int taskId = i;
tasks.Add(Task.Run(async () =>
{
Console.WriteLine($"Task {taskId} starting at {DateTime.Now:HH:mm:ss.fff}");
await service.ProcessDataAsync();
Console.WriteLine($"Task {taskId} completed at {DateTime.Now:HH:mm:ss.fff}");
}));
}
await Task.WhenAll(tasks);
预期结果:只有一个任务实际执行处理逻辑,其他任务应该立即返回。