Books icon indicating copy to clipboard operation
Books copied to clipboard

.NET 9 Lock 类在异步方法中的错误用法及解决方案

Open MarsonShine opened this issue 6 months ago • 0 comments

.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.

问题出现的原因:

  1. Lock 类绑定到特定线程
  2. await 操作可能导致代码在不同线程上恢复执行
  3. 在线程 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();
        // ... 处理逻辑
    }
}

关键要点

  1. 正确做法:在异步方法中使用 SemaphoreSlim 或原子操作
  2. 错误做法:在包含 await 的异步方法中使用 .NET 9 的 Lock
  3. 📖 官方建议:Lock 的 enter 和 exit 之间不应该有 await 操作
  4. 🔧 最佳实践:对于后台任务的单实例控制,推荐使用 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);

预期结果:只有一个任务实际执行处理逻辑,其他任务应该立即返回。

参考文档

MarsonShine avatar Jul 03 '25 10:07 MarsonShine