VB.net 2010 视频教程 VB.net 2010 视频教程 python基础视频教程
SQL Server 2008 视频教程 c#入门经典教程 Visual Basic从门到精通视频教程
当前位置:
首页 > 编程开发 > c#编程 >
  • 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中引用类型赋值是原子的),确保多线程下的一致性。
基础知识拓展

  1. 核心概念对比
概念 适用场景 优点 缺点
lock 同步方法中的互斥锁 简单易用,性能高 不能跨await,阻塞线程
SemaphoreSlim 异步方法中的互斥锁/信号量 支持异步等待,不阻塞线程 需手动释放,避免死锁
ConcurrentDictionary 多线程读写键值对 线程安全,高并发支持 比普通字典略慢(细粒度锁开销)
ImmutableDictionary 读多写少的场景 无锁,线程安全,性能高 写操作创建新对象,内存开销略大
  1. 最佳实践
    优先用并发集合:避免手动加锁,减少出错概率(如用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


相关教程