.NET Core多线程数据库操作:如何避免死锁与并发问题
.NET Core中多线程操作数据库的并发与死锁规避方案
我需要在.NET Core中实现一个方法,启动3个并行线程完成以下逻辑:
- 线程1:检查数据库表T1是否满足condition1,满足则向表T添加一条状态为“waiting”的新行
- 线程2:针对表T2和condition2执行与线程1相同的逻辑
- 线程3:处理表T中所有“waiting”状态的行
请问规避死锁与并发问题的最优方案是什么?
我的现有代码如下:
public void Run() { ThreadPool.QueueUserWorkItem(Thread1); ThreadPool.QueueUserWorkItem(Thread2); ThreadPool.QueueUserWorkItem(Thread3); } private SemaphoreSlim semaphoreSlim = new SemaphoreSlim(1); public void Thread1(object state) { semaphoreSlim.Wait(); try { using (var db = new BDbContext()) { var t1 = db.T1.ToList(); if (condition1(t1)) { var t = f(t1); db.T.Add(t); db.SaveChanges(); } } } catch (Exception ex) { // log } finally { semaphoreSlim.Release(); } } public void Thread2(object state) { semaphoreSlim.Wait(); try { using (var db = new BDbContext()) { var t2 = db.T2.ToList(); if (condition2(t2)) { var t = f(t2); db.T.Add(t); db.SaveChanges(); } } } catch (Exception ex) { // log } finally { semaphoreSlim.Release(); } } public void Thread3(object state) { semaphoreSlim.Wait(); try { using (var db = new BDbContext()) { var tt = db.T.Where(k => k.IsWaiting); //Do something with tt db.SaveChanges(); } } catch (Exception ex) { // log } finally { semaphoreSlim.Release(); } }
现有代码的核心问题
- 全局
SemaphoreSlim把三个线程完全串行化,彻底失去了并行执行的意义,性能极低 - 锁的粒度太大:把查询T1/T2、插入T、处理T的所有操作都锁起来,阻塞时间过长
- 没有利用数据库本身的并发控制能力,完全依赖内存锁,可靠性和扩展性差
最优优化方案
1. 缩小锁粒度,只同步共享资源操作
不需要全局锁,只针对表T的修改操作做同步:
- 线程1/2中,查询T1/T2的操作可以并行执行,不需要加锁;只有当需要向T插入数据时,才进入同步块
- 线程3中,处理T的waiting行时,同样只在修改T的阶段和插入操作同步
示例代码(针对T操作的专用锁):
// 只针对表T的操作加锁,全局共享 private readonly SemaphoreSlim _tOperationLock = new SemaphoreSlim(1); public void Thread1(object state) { try { using (var db = new BDbContext()) { // 查询T1无需锁,并行执行 var t1 = db.T1.ToList(); if (condition1(t1)) { var t = f(t1); db.T.Add(t); // 仅在保存修改时加锁 _tOperationLock.Wait(); try { db.SaveChanges(); } finally { _tOperationLock.Release(); } } } } catch (Exception ex) { // 日志记录 } }
2. 利用数据库行级锁避免并发冲突
内存锁只能控制应用内的并发,分布式场景下失效,更可靠的方式是用数据库的行级锁:
- 线程3处理waiting行时,用带锁的查询锁定目标行,防止被其他线程修改或重复处理(以SQL Server为例):
public void Thread3(object state) { try { using (var db = new BDbContext()) { // 用UPDLOCK+ROWLOCK锁定要处理的行,避免并发修改 var tt = db.T.FromSqlRaw("SELECT * FROM T WHERE IsWaiting = 1 WITH (UPDLOCK, ROWLOCK)").ToList(); // 处理逻辑:比如更新状态、关联其他数据等 foreach (var item in tt) { item.IsWaiting = false; item.ProcessedTime = DateTime.Now; } db.SaveChanges(); } } catch (Exception ex) { // 日志记录 } }
3. 改用异步任务模型(.NET Core最佳实践)
替换ThreadPool.QueueUserWorkItem为Task.Run,结合EF Core的异步方法,提升系统吞吐量:
public async Task RunAsync() { // 三个任务并行执行 await Task.WhenAll( Task.Run(Thread1Async), Task.Run(Thread2Async), Task.Run(Thread3Async) ); } private async Task Thread1Async() { try { using (var db = new BDbContext()) { var t1 = await db.T1.ToListAsync(); if (condition1(t1)) { var t = f(t1); db.T.Add(t); await _tOperationLock.WaitAsync(); try { await db.SaveChangesAsync(); } finally { _tOperationLock.Release(); } } } } catch (Exception ex) { // 日志记录 } }
4. 死锁规避关键原则
- 统一资源访问顺序:如果多个线程需要操作多个表,确保所有线程都按相同顺序访问(比如先操作T1/T2,再操作T,不要交叉)
- 缩短锁持有时间:锁只在必要的数据库修改阶段持有,不要把查询、计算等耗时操作包含在锁内
- 合理设置事务隔离级别:默认的读提交级别已经能避免大部分脏读问题,不要随意提升到可重复读或串行化,否则会增加死锁概率
内容的提问来源于stack exchange,提问作者nonloso
相关产品推荐
相关产品推荐

