C#中Lock语句块抛出异常,如何重构Add方法?
问题描述
我有一个Add方法和ParallelAddAmountTest单元测试,所有功能运行正常,但lock语句块中会抛出异常,这一情况不符合预期。请问该如何重构Add方法?
原Add方法代码:
public void Add(int amountToAdd) { lock (balanceLock) { if (Amount + amountToAdd > MaxAllowedAmount) { throw new ArgumentException("Cannot add the specified amount: the sum exceeds account limit."); } if (Amount + amountToAdd <= MaxAllowedAmount) { AddAmountAndEmulateTransactionDelay(amountToAdd); } } }
对应的单元测试代码:
public void ParallelAddAmountTest() { var balance = new Balance(90000); var addTenThTask = Task.Run(() => balance.Add(10000)); var addFiveThTask = Task.Run(() => balance.Add(10000)); Assert.Throws( typeof(AggregateException), () => Task.WaitAll(addFiveThTask, addTenThTask), "Cannot add the specified amount: the sum exceeds account limit."); Assert.AreEqual(100000, balance.Amount); }
重构方案
核心问题在于:锁内抛出异常是CLR允许的安全行为(会自动释放锁),但如果你期望避免锁内抛异常,或者让逻辑更简洁合理,可以从以下方向调整:
1. 简化冗余判断(保留异常逻辑)
原代码中两个if判断完全冗余,重构后既保留原有异常逻辑,又让代码更清晰:
public void Add(int amountToAdd) { lock (balanceLock) { var newAmount = Amount + amountToAdd; if (newAmount > MaxAllowedAmount) { throw new ArgumentException("Cannot add the specified amount: the sum exceeds account limit."); } AddAmountAndEmulateTransactionDelay(amountToAdd); } }
这种重构不改变原有行为,只是消除重复判断,锁内仍会抛出异常,但这本身是符合业务逻辑的——第二个任务在锁内判断时,余额已经被第一个任务填满,必然触发异常。
2. 用返回值替代异常(避免锁内抛异常)
如果业务允许,将异常改为返回bool表示操作是否成功,彻底避免锁内抛出异常:
public bool Add(int amountToAdd) { lock (balanceLock) { var newAmount = Amount + amountToAdd; if (newAmount > MaxAllowedAmount) { return false; } AddAmountAndEmulateTransactionDelay(amountToAdd); return true; } }
对应的测试需要调整为验证:两个任务中一个返回true、一个返回false,最终余额为100000。
3. 先外部预判断+锁内二次校验(减少锁内异常概率)
先在锁外做一次快速判断,再进入锁内二次校验,减少锁内触发异常的次数,但无法完全避免(仍存在竞态条件):
public void Add(int amountToAdd) { if (Amount + amountToAdd > MaxAllowedAmount) { throw new ArgumentException("Cannot add the specified amount: the sum exceeds account limit."); } lock (balanceLock) { var newAmount = Amount + amountToAdd; if (newAmount > MaxAllowedAmount) { throw new ArgumentException("Cannot add the specified amount: the sum exceeds account limit."); } AddAmountAndEmulateTransactionDelay(amountToAdd); } }
注意:这种方式不能完全消除锁内异常,因为多线程下可能多个线程同时通过外部判断,最终仍会有一个线程在锁内触发异常。
4. 原子操作替代锁(仅适用于简单累加场景)
如果AddAmountAndEmulateTransactionDelay只是简单的金额累加,可使用Interlocked原子操作结合循环重试,避免锁的使用:
public void Add(int amountToAdd) { while (true) { var currentAmount = Amount; var newAmount = currentAmount + amountToAdd; if (newAmount > MaxAllowedAmount) { throw new ArgumentException("Cannot add the specified amount: the sum exceeds account limit."); } // 尝试原子更新余额,成功则退出循环 if (Interlocked.CompareExchange(ref Amount, newAmount, currentAmount) == currentAmount) { // 若有事务延迟等非原子操作,仍需加锁保证原子性 // AddAmountAndEmulateTransactionDelay(amountToAdd); return; } } }
这种方式仅适用于无复杂副作用的金额累加场景,若AddAmountAndEmulateTransactionDelay包含延迟或其他操作,仍需依赖锁。
总结
- 若必须保留异常逻辑,优先选择简化冗余判断的方案,原逻辑本身是正确的,锁内抛异常不会导致死锁;
- 若业务允许,用返回值替代异常是最彻底避免锁内抛异常的方式;
- 原子操作仅适用于简单累加场景,复杂业务逻辑仍需依赖锁。
内容的提问来源于stack exchange,提问作者Pavel.Shymko
相关产品推荐
相关产品推荐

