c# 信号量和锁的区别

信号量和锁的区别

这是一个经典的并发编程问题。简单来说:锁是信号量的一种特殊形式(二进制信号量),但它们在用途和行为上有重要区别。

核心区别一览表

特性 锁 (Lock/Mutex) 信号量 (Semaphore)
所有权 只有获取锁的线程才能释放 任何线程都可以释放
计数 只有0和1两种状态 可以是0到N的任意值
递归 通常支持(同一线程可重复获取) 通常不支持
用途 保护共享资源(互斥) 控制并发数量(限流)
释放者 必须是获取者 可以是任意线程

1. 所有权区别(最重要)

锁:只有所有者能释放

csharp 复制代码
// 锁 - 只有获取锁的线程才能释放
private readonly object _lock = new object();

void LockExample()
{
    lock (_lock)  // 线程A获取锁
    {
        // 只有线程A能释放这个锁
        // 其他线程无法释放
    }  // 线程A释放锁
}

// 错误示例 - 不同线程释放锁会出错
void BadLockExample()
{
    lock (_lock)
    {
        // 尝试在另一个线程释放锁
        Task.Run(() => {
            // 无法释放,因为当前线程没有持有锁
            Monitor.Exit(_lock); // 会抛出异常!
        });
    }
}

信号量:任何线程都能释放

csharp 复制代码
// 信号量 - 任何线程都可以释放
private readonly SemaphoreSlim _semaphore = new SemaphoreSlim(0, 5);

async Task SemaphoreExample()
{
    // 线程A等待
    await _semaphore.WaitAsync();
    
    // 线程B可以释放线程A等待的信号量
    await Task.Run(() => {
        _semaphore.Release();  // 完全合法!
    });
}

// 实际应用:生产者-消费者模式
public class MessageQueue
{
    private SemaphoreSlim _messageCount = new SemaphoreSlim(0, 100);
    
    // 生产者线程添加消息
    public void Produce(string message)
    {
        _queue.Enqueue(message);
        _messageCount.Release(); // 生产者释放信号量
    }
    
    // 消费者线程等待消息
    public async Task<string> ConsumeAsync()
    {
        await _messageCount.WaitAsync(); // 消费者等待信号量
        return _queue.Dequeue();
    }
}

2. 计数区别

锁:二元状态

csharp 复制代码
// 锁只有两种状态:被占用或空闲
private readonly object _lock = new object();

// 状态1: 空闲 (0个线程持有)
// 状态2: 被占用 (1个线程持有)

// 不能有"被2个线程持有"的状态

信号量:多元状态

csharp 复制代码
// 信号量可以有多个计数
private readonly SemaphoreSlim _semaphore = new SemaphoreSlim(3, 5);

// 当前计数可以是 0,1,2,3,4,5
// 3个可用 → 允许3个线程同时进入
// 0个可用 → 所有线程阻塞
// 5个可用 → 允许5个线程同时进入

// 实际应用:限制并发API调用
private readonly SemaphoreSlim _apiLimiter = new SemaphoreSlim(10, 10);

async Task CallApiAsync()
{
    await _apiLimiter.WaitAsync();  // 最多10个并发
    try { await HttpClient.GetAsync(url); }
    finally { _apiLimiter.Release(); }
}

3. 递归获取区别

锁:支持递归(同一线程可重复获取)

csharp 复制代码
// C# 的 lock 支持递归
private readonly object _lock = new object();

void RecursiveLockExample(int depth)
{
    lock (_lock)  // 同一线程可以多次获取同一个锁
    {
        Console.WriteLine($"深度: {depth}");
        if (depth > 0)
            RecursiveLockExample(depth - 1);  // 递归获取
    }
    // 释放次数与获取次数匹配
}

// 调用:同一线程可以成功递归5次
RecursiveLockExample(5);  // ✅ 正常工作

信号量:不支持递归

csharp 复制代码
// 信号量不支持递归
private readonly SemaphoreSlim _semaphore = new SemaphoreSlim(1, 1);

async Task RecursiveSemaphoreExample(int depth)
{
    await _semaphore.WaitAsync();  // 第一次获取成功
    try
    {
        Console.WriteLine($"深度: {depth}");
        if (depth > 0)
        {
            // 第二次获取会死锁!
            await _semaphore.WaitAsync();  // ❌ 永远等待
        }
    }
    finally
    {
        _semaphore.Release();
    }
}

// 正确做法:使用不同的信号量或重构代码
async Task CorrectRecursiveExample(int depth)
{
    await _semaphore.WaitAsync();
    try
    {
        Console.WriteLine($"深度: {depth}");
        if (depth > 0)
        {
            // 递归前先释放,递归后重新获取
            _semaphore.Release();
            await CorrectRecursiveExample(depth - 1);
            await _semaphore.WaitAsync();
        }
    }
    finally
    {
        if (depth == 0) _semaphore.Release();
    }
}

4. 使用场景区别

锁的使用场景:保护共享资源

csharp 复制代码
// 场景1:保护共享数据
public class BankAccount
{
    private readonly object _lock = new object();
    private decimal _balance;
    
    public void Deposit(decimal amount)
    {
        lock (_lock)  // 确保余额操作原子性
        {
            _balance += amount;
        }
    }
}

// 场景2:单例模式
public class Singleton
{
    private static Singleton _instance;
    private static readonly object _lock = new object();
    
    public static Singleton Instance
    {
        get
        {
            lock (_lock)  // 确保只创建一个实例
            {
                return _instance ??= new Singleton();
            }
        }
    }
}

信号量的使用场景:控制并发数量

csharp 复制代码
// 场景1:限制数据库连接数
public class ConnectionPool
{
    private readonly SemaphoreSlim _semaphore;
    private readonly Queue<DbConnection> _connections = new();
    
    public ConnectionPool(int maxConnections)
    {
        _semaphore = new SemaphoreSlim(maxConnections, maxConnections);
    }
    
    public async Task<DbConnection> GetConnectionAsync()
    {
        await _semaphore.WaitAsync();  // 等待可用连接
        return _connections.Dequeue();
    }
}

// 场景2:限流器
public class RateLimiter
{
    private readonly SemaphoreSlim _semaphore;
    private readonly Timer _resetTimer;
    
    public RateLimiter(int maxRequestsPerSecond)
    {
        _semaphore = new SemaphoreSlim(maxRequestsPerSecond, maxRequestsPerSecond);
        _resetTimer = new Timer(_ => 
        {
            // 每秒重置信号量
            var releaseCount = maxRequestsPerSecond - _semaphore.CurrentCount;
            if (releaseCount > 0)
                _semaphore.Release(releaseCount);
        }, null, TimeSpan.Zero, TimeSpan.FromSeconds(1));
    }
    
    public async Task<bool> TryAcquireAsync()
    {
        return await _semaphore.WaitAsync(TimeSpan.Zero);
    }
}

5. 实际对比示例

示例:多线程访问计数器

csharp 复制代码
// 使用锁 - 确保计数器准确
public class CounterWithLock
{
    private int _count = 0;
    private readonly object _lock = new object();
    
    public void Increment()
    {
        lock (_lock)  // 同一时间只有一个线程能修改
        {
            _count++;
        }
    }
    
    public int Value => _count;
}

// 使用信号量 - 也能保护,但过度设计
public class CounterWithSemaphore
{
    private int _count = 0;
    private readonly SemaphoreSlim _semaphore = new SemaphoreSlim(1, 1);
    
    public async Task IncrementAsync()
    {
        await _semaphore.WaitAsync();  // 用信号量模拟锁
        try { _count++; }
        finally { _semaphore.Release(); }
    }
}

示例:Web爬虫控制并发

csharp 复制代码
public class WebCrawler
{
    private readonly HttpClient _httpClient = new();
    private readonly SemaphoreSlim _concurrencyLimiter;  // 用信号量控制并发
    private readonly object _dataLock = new object();    // 用锁保护数据
    private readonly List<string> _results = new();
    
    public WebCrawler(int maxConcurrentRequests)
    {
        _concurrencyLimiter = new SemaphoreSlim(maxConcurrentRequests, maxConcurrentRequests);
    }
    
    public async Task CrawlAsync(List<string> urls)
    {
        var tasks = urls.Select(async url =>
        {
            // 信号量:限制并发请求数
            await _concurrencyLimiter.WaitAsync();
            try
            {
                var content = await _httpClient.GetStringAsync(url);
                
                // 锁:保护共享集合
                lock (_dataLock)
                {
                    _results.Add($"{url}: {content.Length} bytes");
                }
            }
            finally
            {
                _concurrencyLimiter.Release();
            }
        });
        
        await Task.WhenAll(tasks);
    }
}

6. 性能对比

csharp 复制代码
[SimpleJob]
public class LockVsSemaphoreBenchmark
{
    private readonly object _lock = new object();
    private readonly SemaphoreSlim _semaphore = new SemaphoreSlim(1, 1);
    private int _counter;
    
    [Benchmark]
    public int Lock()
    {
        lock (_lock)
        {
            return _counter++;
        }
    }
    
    [Benchmark]
    public async Task<int> Semaphore()
    {
        await _semaphore.WaitAsync();
        try
        {
            return _counter++;
        }
        finally
        {
            _semaphore.Release();
        }
    }
}

// 结果(大约):
// | Method    | Mean      | 
// |-----------|-----------|
// | Lock      | 15 ns     | ← 更快
// | Semaphore | 80 ns     | ← 较慢(约5倍)

选择指南

csharp 复制代码
// 用锁(lock/Mutex)当:
// ✅ 只需要互斥访问(一次一个线程)
// ✅ 保护共享数据(List, Dictionary等)
// ✅ 需要递归锁
// ✅ 追求最高性能

// 用信号量当:
// ✅ 需要限制并发数量(如最多5个连接)
// ✅ 实现生产者-消费者模式
// ✅ 需要跨线程释放(如线程A等待,线程B释放)
// ✅ 实现限流器
// ✅ 需要计数功能

// 实际例子
public class ChoiceExample
{
    // 场景1:保护缓存 - 用锁
    private readonly object _cacheLock = new object();
    private Dictionary<string, object> _cache = new();
    
    public object GetFromCache(string key)
    {
        lock (_cacheLock)  // 用锁,因为只需要互斥
        {
            return _cache.TryGetValue(key, out var value) ? value : null;
        }
    }
    
    // 场景2:限制API调用 - 用信号量
    private readonly SemaphoreSlim _apiLimiter = new SemaphoreSlim(5, 5);
    
    public async Task<string> CallApiAsync(string url)
    {
        await _apiLimiter.WaitAsync();  // 用信号量,限制并发数
        try { return await new HttpClient().GetStringAsync(url); }
        finally { _apiLimiter.Release(); }
    }
}

总结

如果你需要... 使用...
保护共享资源(互斥) 锁 (Lock)
限制并发访问数量 信号量 (Semaphore)
实现生产者-消费者 信号量
跨进程同步 Mutex (锁的一种)
递归获取 Lock (支持递归)
线程A释放线程B等待的资源 信号量
最高性能 Lock (比信号量快)
异步等待 SemaphoreSlim (支持async)

记住这句名言:

"锁是二进制的信号量,但信号量不一定是锁"

  • :强调"互斥" - 一次只能一个人用
  • 信号量:强调"计数" - 一次可以有N个人用
相关推荐
Gofarlic_OMS2 小时前
装备制造企业Fluent许可证成本分点典型案例
java·大数据·开发语言·人工智能·自动化·制造
Freak嵌入式2 小时前
MicroPython LVGL基础知识和概念:显示与多屏管理
开发语言·python·github·php·gui·lvgl·micropython
yu85939582 小时前
matlab雷达信号与干扰的仿真
开发语言·matlab
前进的李工2 小时前
LangChain使用AI工具赋能:解锁大语言模型无限潜力
开发语言·人工智能·语言模型·langchain·大模型
yugi9878382 小时前
C# 串口下载烧写BIN文件工具
开发语言·c#
EAIReport2 小时前
国外网站数据批量采集技术实现路径
开发语言·python
超绝振刀怪2 小时前
【C++可变模板参数】
开发语言·c++·可变模板参数
Freak嵌入式3 小时前
MicroPython LVGL基础知识和概念:时序与动态效果
开发语言·python·github·php·gui·lvgl·micropython