A .NET 10 console application that demonstrates thread safety and deadlock concepts using a BankAccount example.
The app spawns two concurrent tasks that operate on a single bank account:
- Deposit(500) — adds money to the balance
- Withdraw(200) — removes money from the balance (if sufficient funds exist)
Both operations modify shared state (balance), so they need synchronization to avoid race conditions.
The BankAccount class uses a single lock object (lockObj) to guard the critical sections in both Deposit and Withdraw. Because both methods lock on the same object, only one thread can execute either method at a time:
public void Deposit(int amount)
{
lock (lockObj)
{
balance += amount;
}
}
public void Withdraw(int amount)
{
lock (lockObj)
{
if (balance >= amount)
balance -= amount;
}
}
This approach is thread-safe and prevents race conditions.
A deadlock occurs when two or more threads are blocked forever, each waiting for the other to release a resource.
If Deposit and Withdraw used separate lock objects, a classic deadlock could occur:
- Thread A locks the deposit lock and waits for the withdraw lock
- Thread B locks the withdraw lock and waits for the deposit lock
- Both threads block forever
By sharing a single lock object, this implementation guarantees that only one thread holds the lock at any moment, so there is no circular wait and no deadlock.
Deadlock.slnx
DeadlockDemo.Console/
├── BankAccount.cs # BankAccount class with Deposit/Withdraw methods
├── Program.cs # Entry point that runs concurrent tasks
└── DeadlockDemo.Console.csproj
dotnet build
dotnet run --project DeadlockDemo.ConsoleDepositing 500
Balance after deposit: 1500
Withdrawing 200
Balance after withdraw: 1300
The exact order of deposit/withdraw lines may vary between runs because the two tasks run concurrently, but the final balance will always be 1300 (starting 1000 + 500 − 200) thanks to the shared lock.