0

I need to use the lock construction, and edit the following methods to execute in parallel:

    public void Withdraw(int amountToWithdraw)
            {
                if (amountToWithdraw <= 0)
                {
                    throw new ArgumentException("The amount should be greater than 0.");
                }
    
                if (amountToWithdraw > MaxAmountPerTransaction)
                {
                    throw new ArgumentException($"The value {amountToWithdraw} exceeds transaction limit: {MaxAmountPerTransaction}.");
                }
    
                if (amountToWithdraw > Amount)
                {
                    throw new ArgumentException("Insufficient funds.");
                }
                
                WithdrawAndEmulateTransactionDelay(amountToWithdraw);
    
            }

Here is the result

private readonly object balanceLock = new object();
public void Withdraw(int amountToWithdraw)
        {
            if (amountToWithdraw <= 0)
            {
                throw new ArgumentException("The amount should be greater than 0.");
            }

            if (amountToWithdraw > MaxAmountPerTransaction)
            {
                throw new ArgumentException($"The value {amountToWithdraw} exceeds transaction limit: {MaxAmountPerTransaction}.");
            }

            if (amountToWithdraw > Amount)
            {
                throw new ArgumentException("Insufficient funds.");
            }

            lock (balanceLock)
            {
                WithdrawAndEmulateTransactionDelay(amountToWithdraw);
            }

        }

This is a description of the method WithdrawAndEmulateTransactionDelay which shouldn't be changed

private void WithdrawAndEmulateTransactionDelay(int amountToWithdraw)
        {
            Thread.Sleep(1000);
            Amount -= amountToWithdraw;
        }

However, the unit test failed. Where is the mistake in my code?

3
  • Side note: throwing ArgumentOutRangeException is better (more readable and exact) than ArgumentException in the context Commented May 26, 2022 at 10:30
  • @DmitryBychenko ok, but what about the lock? Commented May 26, 2022 at 10:35
  • You should also avoid throwing exceptions. It makes the calling code require exception handling and effectively introduces goto calls in your code. You're better off returning a result from the Withdraw method. Commented May 27, 2022 at 5:06

2 Answers 2

3

It seems, that you should put the last validation within the lock: in your current implementation it's possible that

  1. Thread #1 tries to withdraw cash1, which is valid (cash1 < Account), validation's passed
  2. Thread #2 tries to withdraw cash2, which is valid (cash2 < Account), validation's passed
  3. However cash1 + cash2 > Account
  4. Thread #1 calls for WithdrawAndEmulateTransactionDelay, now Amount == Amount - cash1 < cash2
  5. Thread #2 calls for WithdrawAndEmulateTransactionDelay; since Amount - cash1 < cash2 you have the test failed
private readonly object balanceLock = new object();

public void Withdraw(int amountToWithdraw) {
  // These validations are not depended on Amount, they don't want lock
  if (amountToWithdraw <= 0)
    throw new ArgumentOutOfRangeException(nameof(amountToWithdraw), 
      "The amount should be greater than 0.");

  if (amountToWithdraw > MaxAmountPerTransaction)
    throw new ArgumentOutOfRangeException(nameof(amountToWithdraw), 
      $"The value {amountToWithdraw} exceeds transaction limit: {MaxAmountPerTransaction}.");

  // from now on we start using Amount, so we need the lock:
  lock (balanceLock) {
    if (amountToWithdraw > Amount)
      throw new ArgumentException("Insufficient funds.", nameof(amountToWithdraw));

    WithdrawAndEmulateTransactionDelay(amountToWithdraw);
  } 
}
Sign up to request clarification or add additional context in comments.

Comments

0

I'd also avoid all of those exceptions. Bad input is this code is often to be expected so it's not exceptional.

Try this code:

public TransactionStatus Withdraw(int amountToWithdraw)
{
    bool successful = false;
    string message = "OK";
    int balanceBefore = Amount;
    int balanceAfter = Amount;
    if (amountToWithdraw <= 0)
    {
        message = "The amount should be greater than 0.";
    }
    else if (amountToWithdraw > MaxAmountPerTransaction)
    {
        message = $"The value {amountToWithdraw} exceeds transaction limit: {MaxAmountPerTransaction}.";
    }
    else
    {
        lock (balanceLock)
        {
            if (amountToWithdraw > Amount)
            {
                message = "Insufficient funds.";
            }
            else
            {
                Thread.Sleep(1000);
                Amount -= amountToWithdraw;
                successful = true;
                balanceAfter = Amount;
            }
        }
    }
    return new TransactionStatus()
    {
        Successful = successful, Message = message, BalanceBefore = balanceBefore, BalanceAfter = balanceAfter
    };
}

public struct TransactionStatus
{
    public bool Successful;
    public string Message;
    public int BalanceBefore;
    public int BalanceAfter;
}

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.