Exercise: Thread-Safe Voucher Redemption

Synchronize concurrent threads to prevent race conditions when applying single-use discount vouchers.

Problem statement

In an e-commerce application, a user attempts to exploit a single-use $50 discount voucher by clicking “Apply” simultaneously from their smartphone and their laptop. If the backend does not synchronize the redemption check, both concurrent requests might read the voucher state as “unused” at the exact same millisecond, applying the discount twice. You must synchronize the redemption logic to ensure the voucher is only ever applied once.

Task requirements

  • Implement the RedeemVoucher method inside the VoucherService class.

  • Check if the voucher has already been used.

  • If it is unused, simulate a random processing delay, mark it as used, and return true (success).

  • If it is already used, return false (failure).

  • Ensure the check and the update happen as a single, indivisible operation.

Constraints

  • You must use the lock statement with a dedicated locker object to protect the critical section where the voucher state is checked and modified.

  • You must use Task.WhenAll in the test program to execute the concurrent requests simultaneously.

Good luck trying the exercise! If you’re unsure how to proceed, check the “Solution” tab above.

Get hints

  • Instantiate a private readonly object _voucherLock = new object(); at the class level to use as your lock token.

  • Wrap the if (!IsVoucherUsed) check AND the assignment IsVoucherUsed = true; inside a single lock (_voucherLock) { ... } block. Checking and modifying must be one atomic action.

  • Notice that we use Thread.Sleep instead of Task.Delay. This is because the C# compiler prohibits using the await keyword inside a lock block!

  • In the program file, pass both tasks into Task.WhenAll and await it. It will return a bool[] array containing the results of both requests.

Exercise: Thread-Safe Voucher Redemption

Synchronize concurrent threads to prevent race conditions when applying single-use discount vouchers.

Problem statement

In an e-commerce application, a user attempts to exploit a single-use $50 discount voucher by clicking “Apply” simultaneously from their smartphone and their laptop. If the backend does not synchronize the redemption check, both concurrent requests might read the voucher state as “unused” at the exact same millisecond, applying the discount twice. You must synchronize the redemption logic to ensure the voucher is only ever applied once.

Task requirements

  • Implement the RedeemVoucher method inside the VoucherService class.

  • Check if the voucher has already been used.

  • If it is unused, simulate a random processing delay, mark it as used, and return true (success).

  • If it is already used, return false (failure).

  • Ensure the check and the update happen as a single, indivisible operation.

Constraints

  • You must use the lock statement with a dedicated locker object to protect the critical section where the voucher state is checked and modified.

  • You must use Task.WhenAll in the test program to execute the concurrent requests simultaneously.

Good luck trying the exercise! If you’re unsure how to proceed, check the “Solution” tab above.

Get hints

  • Instantiate a private readonly object _voucherLock = new object(); at the class level to use as your lock token.

  • Wrap the if (!IsVoucherUsed) check AND the assignment IsVoucherUsed = true; inside a single lock (_voucherLock) { ... } block. Checking and modifying must be one atomic action.

  • Notice that we use Thread.Sleep instead of Task.Delay. This is because the C# compiler prohibits using the await keyword inside a lock block!

  • In the program file, pass both tasks into Task.WhenAll and await it. It will return a bool[] array containing the results of both requests.

C# 14.0
using System.Threading;
namespace ECommerce;
public class VoucherService
{
public bool IsVoucherUsed { get; private set; } = false;
// TODO: Create a dedicated private object to serve as the lock token
public bool RedeemVoucher()
{
// TODO: Use the lock statement to safely check and modify IsVoucherUsed
// TODO: Inside the lock, if IsVoucherUsed is false:
// 1. Simulate random processing delay: Thread.Sleep(new Random().Next(10, 100));
// 2. Set IsVoucherUsed to true
// 3. Return true
// TODO: If it is already true, return false
return false;
}
}