Venturing back into C# for the first time in a long time. I'm only 5 minutes in and already felt
compelled to create a new utility class.
Essentially I'm adding an ASP.NET Web API to a daemon process, which requires that the REST API
can get exclusive access to some daemon in-memory state. Since I'm building everything around
async/await, I went looking for the async equivalent of the traditional lock, only to not find
one. From what I could gather, the recommendation is to build one around SemaphoreSlim. After
using that explicitly, I was really uncomfortable with having to make sure that I a) released the
semaphore and b) did it in a try/finally.
I've recently been doing some work in Rust (why that's not my cup of tea is a tale for
another time) and was longing for its Mutex semantics,
in particular that it serves as a guard of the data it provides access to. While part of what makes those semantics
so powerful is the borrow checker, preventing access to the data after relinquishing the lock, the C# equivalent
still feels a lot safer than either using the manual semaphore or even a using(lock) { } type block:
// wrap our state in the lockvarstate=newAsyncLock<SimulationState>(newSimulationState(){Population=100,Iteration=0,SimTime=newDateTime(2024,1,1)});// access the state exclusivelyusingvarguard=awaitstate.AcquireAsync();guard.Value.Iteration++;guard.Value.SimTime=guard.Value.SimTime.AddDays(1);guard.Value.Population+=Random.Shared.Next(-5,10);// We want the state back (for cleanup or whatever)// and make sure it can't be accessed under lock anymorevarrawState=state.ReleaseAndDispose();
The state container is AsyncLock<T> which is IDisposable so that we can clean up the underlying
SemaphoreSlim. It wraps (and implicitly becomes the owner of) whatever state class we want to protect. When we
acquire the state it comes wrapped in an IDisposable container, AsyncLockGuard<T> that on being disposed,
releases the lock again: