Thank you to our sponsors who keep this newsletter free to the reader:
This week's issue is sponsored by QuadSpinner.
In this week's newsletter, we'll see how we can work with locking in .NET 6.
We won't talk about how the lock is actually implemented at the operating system level. Instead, I will focus on application-level locking mechanisms.
Locking allows us to control how many threads can access some piece of code. Why would you want to do this?
Usually because you want to protect access to expensive resources, and you need the concurrency control that locking enables.
We will use a simple BankAccount
class with a Deposit
method
to illustrate how to implement locking.
The C# Lock Statement
The C# language supports locking with the lock
statement.
You can use the lock
statement to define a code block that only one thread can access.
The lock
statement acquires a mutual-exclusion lock (mutex) for a given object,
executes the statement block, and releases the lock.
lock(_lock)
{
// Your code...
}
Here _lock
is a reference type, usually an object
instance.
Let's see how we can implement the BankAccount
class using the lock
statement:
public class BankAccount
{
private static readonly object _lock = new();
private decimal _balance;
public void Deposit(decimal amount)
{
lock(_lock)
{
_balance += amount;
}
}
}
The first thread to reach and execute the lock
statement will be allowed to update
the _balance
. Any other threads will block until the lock is released.
Locking With Semaphore
The Semaphore
class is another option we can use to achieve the same effect.
We'll use the Semaphore
constructor to set the initialCount
to 0,
which means that the Semaphore
is open at the start.
And we will also set the maximumCount
to 1,
which means that only one thread is allowed to enter the Semaphore
.
Let's see how we can implement the BankAccount
class using the Semaphore
:
public class BankAccount
{
private static readonly Semaphore _semaphore = new(
initialCount: 0,
maximumCount: 1);
private decimal _balance;
public void Deposit(decimal amount)
{
_semaphore.WaitOne();
_balance += amount;
_semaphore.Release();
}
}
To enter the Semaphore
, we have to call the WaitOne
method.
If no thread was previously inside, our thread is allowed
to enter the Semaphore
and update the balance.
After updating the balance, we call the Release
method to
release the Semaphore
for other threads that might be waiting.
Asynchronous Locking With SemaphoreSlim
What if we wanted to call an asynchronous method in a locked context?
We can't use the lock
statement as it doesn't support asynchronous calls.
Awaiting an asynchronous call inside a lock
statement will cause a compilation error.
The Semaphore
class can solve this problem.
But I want to show you another option that we have, SemaphoreSlim
.
It's a lightweight alternative to the Semaphore
class and has async
methods.
Let's see how we can implement the BankAccount
class using SemaphoreSlim
:
public class BankAccount
{
private static readonly SemaphoreSlim _semaphore = new(
initialCount: 0,
maximumCount: 1);
private decimal _balance;
public async Task Deposit(decimal amount)
{
await _semaphore.WaitAsync();
_balance += amount;
_semaphore.Release();
}
}
Notice that I updated the Deposit
method to return a Task
.
This time, we're calling WaitAsync
to block the current
thread until it can enter the semaphore.
After updating the balance, we call the Release
method
to release the SemaphoreSlim
like in the previous example.
Are There Other Options For Locking in .NET?
So far I mentioned three options to implement locking:
However, .NET has other classes for concurrency control that you can
explore like
Monitor
,
Mutex
,
ReaderWriterLock
and many more.
I hope you enjoyed this brief introduction to a very complex topic.