1. 多线程数据竞争的根源
想象超市限时抢购的场景:当多个顾客同时伸手去拿最后一瓶酱油时,这种混乱就是典型的数据竞争。在Asp.Net Core中,当多个线程同时修改共享资源(如静态变量、缓存字典、数据库连接池)时,类似问题就会发生。我曾见过一个线上事故:用户积分扣除操作在高并发下出现负数,根源就是未做线程同步。
2. 基础防御手段:锁机制
2.1 lock语句的精准使用
public class PaymentController : ControllerBase
{
// 共享的支付计数器
private static int _paymentCounter = 0;
private static readonly object _locker = new object();
[HttpPost]
public IActionResult ProcessPayment()
{
// 用lock包裹临界区
lock (_locker)
{
if (_paymentCounter >= 100)
{
return BadRequest("达到支付上限");
}
_paymentCounter++;
// 实际支付逻辑...
}
return Ok();
}
}
/* 注意点:
1. lock对象必须是引用类型且私有
2. 避免在lock块内执行耗时操作
3. 不同功能建议使用不同的锁对象 */
2.2 读写锁的智慧选择
当读操作远多于写操作时,ReaderWriterLockSlim
能显著提升性能:
public class ConfigCacheService
{
private static readonly ReaderWriterLockSlim _cacheLock = new();
private static Dictionary<string, string> _configCache = new();
public string GetConfig(string key)
{
_cacheLock.EnterReadLock();
try
{
return _configCache.TryGetValue(key, out var value) ? value : null;
}
finally
{
_cacheLock.ExitReadLock();
}
}
public void UpdateConfig(string key, string value)
{
_cacheLock.EnterWriteLock();
try
{
_configCache[key] = value;
}
finally
{
_cacheLock.ExitWriteLock();
}
}
}
3. 无锁编程的艺术
3.1 原子操作神器Interlocked
public class InventoryService
{
private int _stockCount = 100;
public bool TryReduceStock(int quantity)
{
int original;
int updated;
do
{
original = _stockCount;
if (original < quantity) return false;
updated = original - quantity;
}
while (Interlocked.CompareExchange(ref _stockCount, updated, original) != original);
return true;
}
}
/* 工作原理:
通过CPU级别的原子操作,实现无锁的库存扣减
适用于简单的数值型操作 */
3.2 线程安全集合的妙用
ConcurrentDictionary
在电商购物车场景中的应用:
public class ShoppingCartService
{
private readonly ConcurrentDictionary<int, CartItem> _carts = new();
public void AddItem(int userId, CartItem item)
{
_carts.AddOrUpdate(userId,
id => new ConcurrentBag<CartItem> { item },
(id, existingBag) =>
{
existingBag.Add(item);
return existingBag;
});
}
public IEnumerable<CartItem> GetCart(int userId)
{
return _carts.TryGetValue(userId, out var items)
? items
: Enumerable.Empty<CartItem>();
}
}
4. 异步编程的特殊注意事项
4.1 async/await的陷阱与救赎
错误示例:
private static int _counter = 0;
private static readonly object _locker = new();
public async Task<int> GetNextIdAsync()
{
// 错误!await会释放线程但保持锁
lock (_locker)
{
_counter++;
await SaveToDBAsync(_counter); // 此处可能发生上下文切换
return _counter;
}
}
正确做法:
public async Task<int> GetNextIdAsync()
{
int newId;
lock (_locker)
{
newId = ++_counter;
}
await SaveToDBAsync(newId); // 异步操作放在锁外
return newId;
}
4.2 信号量的异步版本
private static readonly SemaphoreSlim _semaphore = new(1, 1);
public async Task UpdateUserProfileAsync(int userId, ProfileUpdate update)
{
await _semaphore.WaitAsync();
try
{
var user = await _dbContext.Users.FindAsync(userId);
user.Name = update.Name;
await _dbContext.SaveChangesAsync();
}
finally
{
_semaphore.Release();
}
}
5. 进阶模式:不可变数据结构
在配置中心等场景中,使用不可变集合避免锁:
public class AppConfigProvider
{
private ImmutableDictionary<string, string> _configs = ImmutableDictionary<string, string>.Empty;
public void ReloadConfigs(Dictionary<string, string> newConfigs)
{
var builder = _configs.ToBuilder();
foreach (var kv in newConfigs)
{
builder[kv.Key] = kv.Value;
}
// 原子替换引用
Interlocked.Exchange(ref _configs, builder.ToImmutable());
}
public string GetConfig(string key)
{
// 无需锁,始终读取完整快照
return _configs.TryGetValue(key, out var value) ? value : null;
}
}
技术选型决策树
- 读多写少 → 读写锁/不可变集合
- 简单数值操作 → Interlocked
- 高频修改的字典 → ConcurrentDictionary
- 需要等待资源的异步操作 → SemaphoreSlim
- 复杂业务对象 → lock+最小临界区
性能优化黄金法则
- 锁的粒度要尽可能细
- 临界区代码不超过1毫秒
- 避免在锁内调用外部服务
- 使用
Monitor.TryEnter
设置超时 - 通过
ConcurrentQueue
实现生产者消费者模式
血泪经验总结
- 曾经因为未释放ReaderWriterLockSlim导致线程池耗尽
- 在循环中错误使用lock引发死锁
- async方法内误用lock导致性能悬崖
- 忘记Interlocked的内存屏障特性导致数据不一致
- 过度依赖线程安全集合引发内存泄漏
终极解决方案
对于分布式场景,建议结合Redis分布式锁+本地锁的双重校验机制。本地锁解决单实例内的竞争,Redis锁保证跨实例的原子性,但要注意时钟漂移和锁续期问题。
多线程编程就像高空走钢丝,既要保持平衡(性能),又要确保安全(数据一致性)。掌握这些技巧后,相信你在Asp.Net Core的并发世界里定能游刃有余。记住:没有银弹,只有适合场景的解决方案。