信号量和锁的区别
这是一个经典的并发编程问题。简单来说:锁是信号量的一种特殊形式(二进制信号量),但它们在用途和行为上有重要区别。
核心区别一览表
| 特性 | 锁 (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个人用