using System;
using System.Threading;
public sealed class MutexLock<T>
{
internal readonly object _lock = new object();
internal T _guardedValue;
public MutexLock()
{
}
public MutexLock(T value)
{
_guardedValue = value;
}
public AcquiredMutex<T> Acquire()
{
Monitor.Enter(_lock);
return new AcquiredMutex<T>(this);
}
}
public struct AcquiredMutex<T>:
IDisposable
{
private MutexLock<T> _owner;
internal AcquiredMutex(MutexLock<T> owner)
{
_owner = owner;
}
public void Dispose()
{
var owner = _owner;
if (owner == null)
return;
_owner = null;
Monitor.Exit(owner._lock);
}
public ref T Value
=> ref _owner._guardedValue;
// Because this struct is going to be used with the "using" clause, the
// property Value is assumed as readonly by the compiler, while the return
// of a method that does exactly the same is not.
// Yet, members of Value (if it has mutable fields or properties) don't suffer
// such a readonly enforcement from the compiler.
public ref T MutableValue()
=> ref _owner._guardedValue;
}
class Program {
static void Main()
{
var mutex = new MutexLock<(int id, string name)>((12, "This is a string"));
// The following would cause a compile-time error.
//Console.WriteLine(mutex.Value);
using (var locked = mutex.Acquire())
{
Console.WriteLine(locked.Value);
// Because of an annoying trait of using(), the compiler assumes we
// cannot modify any field or property of "locked". Yet, a ref result from
// a method, not a property, is OK... that's why I added that
// MutableValue method.
locked.MutableValue() = (15, "Test");
// Just showing that the value was updated.
Console.WriteLine(locked.Value);
// Just to show that the locks work, we are scheduling an item to Run
// that will also use the MutexLock... but then we will wait for 3 seconds
// before releasing the lock. For this sample, it should force the
// secondary thread to wait and prove that the locks are working.
ThreadPool.QueueUserWorkItem
(
(_) =>
{
using (var newLocked = mutex.Acquire())
Console.WriteLine("Secondary thread acquired the lock. Values are: " + newLocked.Value);
}
);
Thread.Sleep(3000);
// Notice that we only change things at the end of the scope. So, the
// secondary thread should read the new name. Also notice that Updating
// a field of Value (instead of trying to replace Value) is allowed.
locked.Value.id = 16;
locked.Value.name = "Updating a field of Value.";
}
Thread.Sleep(1000);
}
}