1. 当多线程遇见共享资源:一场必须解决的"修罗场"
在ASP.NET Core开发中,我们常常会遇到这样的场景:订单处理系统需要同时更新库存,实时聊天室要处理并发的消息推送,分布式缓存需要同步多个节点的数据更新。这些场景就像超市收银台的混乱现场——如果所有人都同时伸手拿货架上的商品,必然会出现计算错误、数据混乱等问题。
最近,在处理一个秒杀系统时,我们遇到了典型的并发问题:当1000个用户同时抢购最后10件商品时,库存出现了-5件的诡异数据。这就是典型的线程竞争问题,不同线程同时读取并修改共享资源导致的灾难。
// 问题示例:线程不安全的库存扣减
public class UnsafeInventoryService
{
private int _stock = 10;
public void DeductStock()
{
if (_stock > 0)
{
Thread.Sleep(10); // 模拟处理耗时
_stock -= 1;
Console.WriteLine($"剩余库存:{_stock}");
}
}
}
// 当10个线程同时执行时,可能出现:
// 剩余库存:9
// 剩余库存:8
// ...
// 剩余库存:-5 <- 灾难发生了!
2. ASP.NET Core同步机制兵器库
2.1 基础护盾:lock关键字
就像超市储物柜的钥匙,lock是最简单的互斥解决方案。它相当于给代码段加上"施工中,请绕行"的告示牌。
public class LockInventoryService
{
private readonly object _lockObj = new object();
private int _stock = 10;
public void DeductStock()
{
lock (_lockObj)
{
if (_stock > 0)
{
Thread.Sleep(10);
_stock -= 1;
Console.WriteLine($"安全库存:{_stock}");
}
}
}
}
// 适用场景:简单的临界区保护
// 优点:使用简单,性能较好
// 缺点:无法跨方法控制,容易造成死锁
2.2 灵活卫士:Monitor类
如果说lock是自动门,那么Monitor就是带密码锁的手动门。它提供了更精细的控制能力,比如超时机制。
public class MonitorInventoryService
{
private readonly object _monitorObj = new object();
private int _stock = 10;
public bool TryDeductStock(int timeoutMs = 100)
{
if (Monitor.TryEnter(_monitorObj, timeoutMs))
{
try
{
if (_stock > 0)
{
Thread.Sleep(10);
_stock -= 1;
Console.WriteLine($"监控库存:{_stock}");
return true;
}
return false;
}
finally
{
Monitor.Exit(_monitorObj);
}
}
return false;
}
}
// 适用场景:需要超时控制的资源访问
// 优点:避免死锁,更灵活
// 缺点:需要手动管理退出
2.3 跨界特工:Mutex
当需要跨进程同步时,Mutex就像连接不同大楼的对讲系统,可以协调不同应用程序之间的资源访问。
public class MutexInventoryService : IDisposable
{
private Mutex _mutex = new Mutex(false, "Global\\MyAppInventory");
private int _stock = 10;
public void DeductStock()
{
if (_mutex.WaitOne(1000))
{
try
{
if (_stock > 0)
{
Thread.Sleep(10);
_stock -= 1;
Console.WriteLine($"跨进程库存:{_stock}");
}
}
finally
{
_mutex.ReleaseMutex();
}
}
}
public void Dispose() => _mutex?.Dispose();
}
// 适用场景:分布式系统或跨进程同步
// 优点:支持跨进程,系统级锁
// 缺点:性能较低,需要处理异常
2.4 流量警察:Semaphore
像十字路口的红绿灯,Semaphore可以控制同时访问资源的线程数量,特别适合资源池管理。
public class SemaphoreInventoryService
{
private SemaphoreSlim _semaphore = new SemaphoreSlim(3, 3);
private int _stock = 10;
public async Task DeductStockAsync()
{
await _semaphore.WaitAsync();
try
{
if (_stock > 0)
{
await Task.Delay(10);
_stock -= 1;
Console.WriteLine($"信号量库存:{_stock}");
}
}
finally
{
_semaphore.Release();
}
}
}
// 适用场景:限制并发访问数量
// 优点:支持异步,可控制并发量
// 缺点:不保证执行顺序
2.5 读写判官:ReaderWriterLockSlim
像图书馆的借阅系统,区分读者和写者,解决读写竞争问题。
public class CacheService
{
private ReaderWriterLockSlim _cacheLock = new ReaderWriterLockSlim();
private Dictionary<string, string> _cache = new Dictionary<string, string>();
public string Get(string key)
{
_cacheLock.EnterReadLock();
try
{
return _cache.TryGetValue(key, out var value) ? value : null;
}
finally
{
_cacheLock.ExitReadLock();
}
}
public void Update(string key, string value)
{
_cacheLock.EnterWriteLock();
try
{
_cache[key] = value;
}
finally
{
_cacheLock.ExitWriteLock();
}
}
}
// 适用场景:读多写少的场景
// 优点:提升读性能,减少等待
// 缺点:实现复杂度较高
3. 选择武器的决策树
根据实际场景选择同步机制时,可以参考以下决策流程:
需要跨进程同步吗?
- 是 → 使用Mutex
- 否 → 进入下一步
需要限制并发数量吗?
- 是 → 使用Semaphore
- 否 → 进入下一步
是读多写少的场景吗?
- 是 → 使用ReaderWriterLockSlim
- 否 → 进入下一步
需要超时控制吗?
- 是 → 使用Monitor
- 否 → 使用lock
4. 实战避坑指南
4.1 死锁预防三原则
- 锁顺序规则:所有线程按相同顺序获取锁
- 超时机制:使用TryEnter替代Enter
- 资源分层:将大锁拆分为多个小锁
4.2 性能优化技巧
- 锁粒度控制:库存服务中按商品ID分桶加锁
- 无锁编程:使用Interlocked类实现原子操作
- 异步同步:SemaphoreSlim的WaitAsync方法
// 分桶锁示例
public class BucketLockInventoryService
{
private readonly object[] _lockBuckets;
private int[] _stocks;
public BucketLockInventoryService(int bucketSize = 10)
{
_lockBuckets = Enumerable.Range(0, bucketSize)
.Select(_ => new object()).ToArray();
_stocks = new int[bucketSize];
Array.Fill(_stocks, 10);
}
public void DeductStock(int productId)
{
var bucketIndex = productId % _lockBuckets.Length;
lock (_lockBuckets[bucketIndex])
{
if (_stocks[bucketIndex] > 0)
{
_stocks[bucketIndex] -= 1;
}
}
}
}
5. 关联技术:并发集合类
当同步机制显得笨重时,不妨考虑System.Collections.Concurrent命名空间下的线程安全集合:
public class ConcurrentCache
{
private ConcurrentDictionary<string, string> _cache = new ConcurrentDictionary<string, string>();
// 自动处理并发的添加或更新
public void AddOrUpdate(string key, string value)
{
_cache.AddOrUpdate(key, value, (k, old) => value);
}
// 线程安全的读取操作
public bool TryGetValue(string key, out string value)
{
return _cache.TryGetValue(key, out value);
}
}
6. 总结:没有银弹,只有合适的工具
通过多个实际案例的分析,我们可以得出以下结论:
- 简单临界区优先选择lock
- 需要超时控制使用Monitor
- 跨进程场景选择Mutex
- 流量控制用Semaphore
- 读写分离场景用ReaderWriterLockSlim
最后记住:最好的同步就是不同步。在ASP.NET Core开发中,合理使用异步编程模型、采用无状态设计、利用消息队列解耦,往往能从架构层面减少对同步机制的依赖。但当必须面对共享资源时,选择合适的同步工具就像选择合适的手术刀——既要足够锋利,又要避免误伤自己。