When accessing a shared resource, you can use the lock statement or a synchronization primitive such as Mutex. However, in complex code, it's easy to forget to acquire the lock:
C#
var obj = new object();
var value = 42;
lock (obj)
{
// You need to ensure you use lock everywhere you access the shared resource
Console.WriteLine(value);
}
// ⚠️ You can access the resource without a lock
value = 43;
You can make it more explicit and less error-prone by creating a Mutex<T> class that encapsulates the shared resource and the synchronization primitive.
C#
var mutex = new Mutex<int>(42);
using (var mutexScope = mutex.Acquire())
{
// Access the shared resource
Console.WriteLine(mutexScope.Value);
// Update the shared resource
mutexScope.Value = 43;
}
// ✔️ You cannot use the shared resource outside the mutex scope
Note that this is a mitigation, not a fully robust solution. You can still access the shared resource outside the scope by copying the data inside the scope and using it elsewhere:
C#
var mutex = new Mutex<MyData>();
MyData escapedData;
using (var mutexScope = mutex.Acquire())
{
// Update the shared resource
escapedData = mutexScope.Value;
}
// ⚠️ You can use the shared resource outside the mutex scope
escapedData.Value = 42;
Here's the implementation of the Mutex<T> class:
C#
sealed class Mutex<T>
{
internal T _value;
private readonly Lock _lock = new();
public Mutex() { _value = default!; }
public Mutex(T initialValue) => _value = initialValue;
public MutexScope<T> Acquire()
{
_lock.Enter();
return new MutexScope<T>(this);
}
internal void Release() => _lock.Exit();
}
sealed class MutexScope<T> : IDisposable
{
private readonly Mutex<T> mutex;
private bool disposed;
internal MutexScope(Mutex<T> mutex)
{
this.mutex = mutex;
}
public ref T Value
{
get
{
ObjectDisposedException.ThrowIf(disposed, this);
return ref mutex._value!;
}
}
public void Dispose()
{
mutex.Release();
disposed = true;
}
}
Do you have a question or a suggestion about this post? Contact me!