You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.04 03:10:13