lock与SemaphoreSlim的区别与应用

在多线程编程中,线程同步是确保数据一致性和避免竞争条件的重要手段。本文将深入探讨 lock(object)SemaphoreSlim 这两种常用的同步机制,详细分析它们的区别、适用场景以及如何在实际开发中选择合适的同步工具。

一、lock(object)(或 Monitor

1. 单线程访问: lock 关键字用于确保在同一时间只有一个线程能够访问被保护的代码块。它实现了互斥锁(mutex),适用于单线程临界区的保护。这意味着当一个线程进入锁定区域时,其他试图进入该区域的线程将被阻塞,直到锁被释放。

2. 锁范围: lock 保护的代码块只能在单个线程内运行,进入锁定区域的线程会被阻塞直到锁被释放。它提供了一种简单而有效的方式来防止多个线程同时访问共享资源。

3. 实现方式: lock 是基于 Monitor 类的,它是CLR提供的一种基础同步机制。Monitor 提供了 EnterExit 方法来显式地进入和离开临界区,lock 关键字在语法上对其进行了简化,使得代码更加易读。

4. 简单易用: lock 语法简洁,易于使用,适用于简单的线程同步需求。典型的用法如下:

private static readonly object _lockObject = new object();

public void SomeMethod()
{
    lock (_lockObject)
    {
        // Critical section.
    }
}

5. 不支持异步: lock 不能用于异步代码块,不能与 await 一起使用。这意味着在需要异步处理的场景中,lock 并不适用。

二、SemaphoreSlim

1. 多线程访问: SemaphoreSlim 允许指定同时可以访问资源的线程数。它可以用作计数信号量,允许多个线程并发访问指定数量的资源。例如,初始化为1的 SemaphoreSlim 等价于一个互斥锁,而初始化为大于1的 SemaphoreSlim 则允许指定数量的线程并发访问。

2. 锁范围: SemaphoreSlim 可以控制同时访问资源的多个线程,适用于需要限制并发访问数量的场景。它在资源访问控制方面提供了更大的灵活性。

3. 实现方式: SemaphoreSlim 是一个轻量级的、基于信号量的同步机制。它支持异步操作,使其在需要控制并发访问的异步编程中尤为适用。

4. 异步支持: SemaphoreSlim 提供了异步等待功能,可以与 asyncawait 关键字一起使用。这使得它非常适用于异步编程模型,能够有效避免异步方法中的阻塞问题。

private static readonly SemaphoreSlim _semaphore = new SemaphoreSlim(1, 1);

public async Task SomeMethodAsync()
{
    await _semaphore.WaitAsync();
    try
    {
        // Critical section.
    }
    finally
    {
        _semaphore.Release();
    }
}

5. 复杂使用场景: SemaphoreSlim 适用于更复杂的并发控制需求,如限制并发访问数量或需要异步支持的场景。它能够根据具体的并发需求,灵活调整允许并发访问的线程数量。

6.工作原理:

  1. WaitAsync 方法
    • 当调用 WaitAsync 方法时,如果信号量的计数大于 0,则计数会减 1,并且立即允许调用线程进入临界区。
    • 如果信号量的计数为 0,则调用线程会进入等待状态,直到有其他线程调用 Release 方法增加了计数,或者超时。
  2. Release 方法
    • 调用 Release 方法会增加信号量的计数。如果有线程正在等待进入临界区,则会释放其中一个线程,使其可以进入临界区执行任务。
    • 如果没有等待的线程,则信号量的计数会累加,超过 SemaphoreSlim(int initialCount, int maxCount)其中的maxCount 则会抛出 SemaphoreFullException

三、实际应用:AsyncLoadHelper<TData>

在实际开发中,我们常常需要在异步方法中进行线程同步。下面是一个 AsyncLoadHelper<TData> 类的实现,它通过 SemaphoreSlim 确保数据加载操作的线程安全性,并且支持异步操作。

public class AsyncLoadHelper<TData> : BindableBase, IDisposable
{
    private TData _data;
    private bool _isLoading;
    private Exception _loadingException;
    private readonly Lazy<DelegateCommand> _loadCommand;
    private readonly Func<CancellationToken, Task<TData>> _dataLoadMethod;
    private CancellationTokenSource _cts;
    private readonly SemaphoreSlim _asyncLock = new SemaphoreSlim(1, 1);

    public TData Data
    {
        get => _data;
        set => SetProperty(ref _data, value);
    }

    public bool IsLoading
    {
        get => _isLoading;
        set => SetProperty(ref _isLoading, value);
    }

    public Exception LoadingException
    {
        get => _loadingException;
        set => SetProperty(ref _loadingException, value);
    }

    public DelegateCommand LoadCommand => _loadCommand.Value;

    public AsyncLoadHelper(Func<CancellationToken, Task<TData>> dataLoadMethod)
    {
        _dataLoadMethod = dataLoadMethod;
        _loadCommand = new Lazy<DelegateCommand>(() =>
            new DelegateCommand(async () => await ExecuteLoadDataAsync(), () => !IsLoading).ObservesProperty(() => IsLoading));
    }

    public virtual async Task ExecuteLoadDataAsync()
    {
        if (IsLoading) return;
        await _asyncLock.WaitAsync();

        _cts?.Cancel();
        _cts = new CancellationTokenSource();

        IsLoading = true;
        LoadingException = null;

        try
        {
            Data = await _dataLoadMethod(_cts.Token);
        }
        catch (OperationCanceledException)
        {
            // Handle if needed
        }
        catch (Exception e)
        {
            LoadingException = e;
        }
        finally
        {
            IsLoading = false;
            _asyncLock.Release();
        }
    }

    public void Dispose()
    {
        _cts?.Cancel();
        _cts?.Dispose();
        _asyncLock.Dispose();
    }
}

关键点

  • 防止重复加载:使用 IsLoading 防止在加载过程中多次调用 ExecuteLoadDataAsync

  • 同步机制:通过 await _asyncLock.WaitAsync() 确保在任何时候只有一个线程能够进入临界区。

  • 释放信号量:无论加载操作是否成功,finally 块中的 _asyncLock.Release() 确保了信号量总是被释放,从而不会阻塞后续的加载请求。

  • 简单同步:如果需要简单的、单线程的临界区保护,lock 是更简单和直接的选择。它的语法简洁,易于理解和使用,非常适合基本的线程同步需求。

  • 并发控制和异步支持:如果需要控制同时访问资源的线程数量,或者需要在异步代码中使用,SemaphoreSlim 是更为合适的选择。它不仅支持异步操作,还能灵活地控制并发线程的数量,适用于更复杂的同步场景。

posted @ 2024-05-29 10:47  非法关键字  阅读(14)  评论(0编辑  收藏  举报