-
C#异步锁与线程安全——共享资源保护
第一部分:C#基础入门
异步锁与线程安全——共享资源保护
实例介绍
在日常开发中,你一定遇到过这些场景:
电商平台上,100个用户同时下单购买同一商品(库存100),结果超卖导致库存变成负数;
多线程读写共享字典时,突然抛出InvalidOperationException(集合已修改,无法枚举);
异步方法中用lock关键字,编译器直接报错,或者运行时出现奇怪的线程阻塞问题。
这些都是线程安全问题——当多个线程/任务同时操作共享资源(如库存、字典)时,若没有合适的保护机vb.net教程C#教程python教程SQL教程access 2010教程制,会导致数据不一致、异常甚至程序崩溃。本节将通过3个典型场景,带你掌握异步锁、并发集合、不可变对象等线程安全解决方案,让你的代码在高并发下依然稳定。
需求分析
本节需解决3类核心问题:
1.异步方法中的共享资源保护:比如库存扣减场景,异步方法中不能直接用lock,需用异步锁避免超卖;
2.多线程安全读写键值对:普通Dictionary在多线程下会崩溃,需用ConcurrentDictionary替代;
3.无锁场景的实现:用不可变对象消除锁的需求,适合读多写少的场景(如配置信息)。
同时,需明确错误写法的危害:比如用lock在异步方法中阻塞线程、用普通字典导致数据错乱、忽略不可变对象导致锁过度使用。
代码实现
前置条件:.NET 6+;需引用System.Collections.Concurrent(并发集合)、System.Collections.Immutable(不可变对象)。
场景1:异步锁解决库存超卖问题
错误写法:用lock在异步方法中(编译器报错+线程阻塞)
csharp
private static int _stock = 100; // 共享库存
private static readonly object _lockObj = new object();
// 错误:lock不能跨await,编译器直接提示“不能在lock语句体中使用await表达式”
public async Task DeductStockWrongAsync(int amount)
{
lock (_lockObj)
{
if (_stock >= amount)
{
await Task.Delay(100); // 模拟扣减库存的异步IO(如数据库操作)
_stock -= amount;
}
}
}
问题:lock是同步锁,绑定线程且阻塞线程;异步方法中await会释放线程,导致lock的作用域被打破,编译器直接禁止这种写法。
正确写法:用SemaphoreSlim实现异步锁
csharp
private static int _stock = 100;
// 异步锁:初始信号量1,最大1(互斥锁,等价于lock)
private static readonly SemaphoreSlim _asyncLock = new SemaphoreSlim(1, 1);
// 正确:异步锁保护共享库存
public async Task DeductStockCorrectAsync(int amount)
{
// 异步等待获取锁(不阻塞线程)
await _asyncLock.WaitAsync();
try
{
if (_stock >= amount)
{
await Task.Delay(100); // 模拟异步IO
_stock -= amount;
Console.WriteLine($"扣减成功,剩余库存:{_stock}");
}
else
{
Console.WriteLine("库存不足,扣减失败");
}
}
finally
{
// 释放锁(必须在finally中,避免异常导致死锁)
_asyncLock.Release();
}
}
// 测试:100个并发请求扣减1个库存
public static async Task TestStockDeduction()
{
var tasks = new List<Task>();
for (int i = 0; i < 100; i++)
{
tasks.Add(DeductStockCorrectAsync(1));
}
await Task.WhenAll(tasks);
Console.WriteLine($"最终库存:{_stock}"); // 正确结果:0(无超卖)
}
场景2:并发字典解决多线程读写问题
错误写法:用普通Dictionary在多线程下读写
csharp
private static Dictionary<int, string> _products = new Dictionary<int, string>();
// 错误:普通Dictionary非线程安全,多线程下Add会抛出异常
public void AddProductWrong(int id, string name)
{
if (!_products.ContainsKey(id))
{
_products.Add(id, name); // 多线程同时进入时,抛出“键已存在”异常
}
}
// 测试:1000个线程同时添加商品
public static void TestDictionaryWrong()
{
var tasks = new List<Task>();
for (int i = 0; i < 1000; i++)
{
int id = i;
tasks.Add(Task.Run(() => AddProductWrong(id, $"商品{id}")));
}
Task.WaitAll(tasks); // 运行时抛出ArgumentException:键已存在
}
问题:普通Dictionary的Add、Remove方法非线程安全,多线程下会出现竞态条件(如两个线程同时判断键不存在,然后同时Add)。
正确写法:用ConcurrentDictionary实现线程安全读写
csharp
// 线程安全的并发字典
private static ConcurrentDictionary<int, string> _concurrentProducts = new ConcurrentDictionary<int, string>();
// 正确:用TryAdd避免竞态条件
public bool AddProductCorrect(int id, string name)
{
return _concurrentProducts.TryAdd(id, name); // 线程安全添加,返回是否成功
}
// 正确:用GetOrAdd实现“不存在则添加,存在则返回”
public string GetOrAddProduct(int id, string name)
{
// 若id不存在,添加并返回name;若存在,返回现有值(线程安全)
return _concurrentProducts.GetOrAdd(id, name);
}
// 测试:1000个线程同时添加商品
public static void TestDictionaryCorrect()
{
var tasks = new List<Task>();
for (int i = 0; i < 1000; i++)
{
int id = i;
tasks.Add(Task.Run(() => AddProductCorrect(id, $"商品{id}")));
}
Task.WaitAll(tasks);
Console.WriteLine($"最终商品数量:{_concurrentProducts.Count}"); // 正确结果:1000
}
场景3:不可变对象消除锁的需求
错误写法:用可变对象+锁保护读多写少的场景
csharp
private static Dictionary<string, string> _config = new Dictionary<string, string>();
private static readonly object _configLock = new object();
// 错误:读多写少场景下,锁会导致性能瓶颈
public string GetConfigWrong(string key)
{
lock (_configLock) // 读操作也加锁,浪费性能
{
_config.TryGetValue(key, out var value);
return value;
}
}
public void UpdateConfigWrong(string key, string value)
{
lock (_configLock)
{
_config[key] = value;
}
}
问题:读操作占比90%以上时,锁会阻塞大量读线程,导致性能下降。
正确写法:用ImmutableDictionary实现无锁读写
csharp
// 不可变字典:一旦创建,无法修改(修改时返回新对象)
private static ImmutableDictionary<string, string> _immutableConfig = ImmutableDictionary<string, string>.Empty;
// 正确:读操作无锁,线程安全
public string GetConfigCorrect(string key)
{
_immutableConfig.TryGetValue(key, out var value);
return value;
}
// 正确:写操作返回新对象,原对象不变
public void UpdateConfigCorrect(string key, string value)
{
// SetItem返回新的不可变字典,原字典不受影响
_immutableConfig = _immutableConfig.SetItem(key, value);
}
// 测试:1000个读线程+10个写线程
public static void TestImmutableConfig()
{
// 写线程:更新配置
var writeTasks = Enumerable.Range(0,10).Select(i => Task.Run(() =>
UpdateConfigCorrect($"key{i}", $"value{i}")));
// 读线程:读取配置
var readTasks = Enumerable.Range(0,1000).Select(i => Task.Run(() =>
GetConfigCorrect($"key{i%10}")));
Task.WaitAll(writeTasks.Concat(readTasks).ToArray());
Console.WriteLine($"最终配置数量:{_immutableConfig.Count}"); // 正确结果:10
}
逐行讲解
场景1:异步锁解决库存超卖
1.SemaphoreSlim初始化:new SemaphoreSlim(1,1)表示初始信号量为1,最大信号量为1(等价于互斥锁),适合异步场景。
2.WaitAsync:异步等待获取锁,不阻塞线程(区别于lock的同步阻塞)。
3.try-finally块:确保无论是否抛出异常,锁都会被释放(Release),避免死锁。
4.库存扣减:在锁保护下,只有一个任务能修改_stock,避免超卖。
场景2:并发字典的正确使用
1.ConcurrentDictionary:线程安全的字典,内部通过细粒度锁实现高并发。
2.TryAdd:尝试添加键值对,返回是否成功(避免ContainsKey+Add的竞态条件)。
3.GetOrAdd:原子操作,解决“不存在则添加,存在则返回”的常见需求,无需手动加锁。
场景3:不可变对象的无锁读写
1.ImmutableDictionary:不可变对象,一旦创建就无法修改。修改时(如SetItem)返回新的对象,原对象保持不变。
2.读操作无锁:因为原对象不会被修改,多线程读取时无需锁,性能更高。
3.写操作原子性:SetItem返回新对象,赋值操作是原子的(.NET中引用类型赋值是原子的),确保多线程下的一致性。
基础知识拓展
- 核心概念对比
| 概念 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| lock | 同步方法中的互斥锁 | 简单易用,性能高 | 不能跨await,阻塞线程 |
| SemaphoreSlim | 异步方法中的互斥锁/信号量 | 支持异步等待,不阻塞线程 | 需手动释放,避免死锁 |
| ConcurrentDictionary | 多线程读写键值对 | 线程安全,高并发支持 | 比普通字典略慢(细粒度锁开销) |
| ImmutableDictionary | 读多写少的场景 | 无锁,线程安全,性能高 | 写操作创建新对象,内存开销略大 |
-
最佳实践
优先用并发集合:避免手动加锁,减少出错概率(如用ConcurrentQueue代替Queue,ConcurrentStack代替Stack)。
异步方法禁用lock:改用SemaphoreSlim或第三方库(如Nito.AsyncEx.AsyncLock)实现异步锁。
读多写少用不可变对象:如配置信息、缓存数据,用ImmutableArray/ImmutableDictionary消除锁的需求。
锁粒度要小:避免在锁内做耗时操作(如IO、网络请求),减少阻塞时间。
避免嵌套锁:嵌套锁容易导致死锁(如线程A持有锁1等待锁2,线程B持有锁2等待锁1)。
总结
本节通过3个场景,带你掌握了线程安全的核心解决方案:
1.异步锁:用SemaphoreSlim解决异步方法中的共享资源保护(如库存扣减);
2.并发集合:用ConcurrentDictionary替代普通字典,实现多线程安全读写;
3.不可变对象:用ImmutableDictionary消除锁的需求,适合读多写少场景。
线程安全的核心是避免共享可变状态——优先用并发集合和不可变对象,必要时用异步锁,禁用lock在异步方法中。掌握这些方法,你的代码在高并发下会更稳定、更高效。
本站原创,转载请注明出处:https://www.xin3721.com/ArticlecSharp/c49450.html










