I'm thinking about an approach to use the IDisposable pattern to synchonize/coordinate access to a shared resource.
Here's my code so far (easy to run with LinqPad):
#define WITH_CONSOLE_LOG
//better undefine WITH_CONSOLE_LOG when testing long loops
public abstract class SynchronizedAccessBase
{
private readonly object syncObj = new();
private class AccessToken : IDisposable
{
private SynchronizedAccessBase parent;
private bool didDispose;
public AccessToken(SynchronizedAccessBase parent)
{
this.parent = parent;
}
protected virtual void Dispose(bool disposing)
{
if (!this.didDispose)
{
Monitor.Exit(this.parent.syncObj);
#if WITH_CONSOLE_LOG
Console.WriteLine("Monitor.Exit by Thread=" + Thread.CurrentThread.ManagedThreadId);
#endif
if (disposing)
{
//nothing specific here
}
this.didDispose = true;
}
}
~AccessToken()
{
this.Dispose(disposing: false);
}
void IDisposable.Dispose()
{
Dispose(disposing: true);
GC.SuppressFinalize(this);
}
}
public IDisposable WantAccess()
{
Monitor.Enter(this.syncObj);
#if WITH_CONSOLE_LOG
Console.WriteLine("Monitor.Enter by Thread=" + Thread.CurrentThread.ManagedThreadId);
#endif
return new AccessToken(this);
}
}
public class MyResource : SynchronizedAccessBase
{
public int Value;
}
private MyResource TheResource;
private void MyMethod()
{
using var token = TheResource.WantAccess(); //comment out this line to see the unsynced behavior
#if WITH_CONSOLE_LOG
Console.WriteLine("Inc'ing Value by Thread=" + Thread.CurrentThread.ManagedThreadId);
#endif
TheResource.Value++;
}
void Main()
{
this.TheResource = new MyResource();
var inc_tasks = new Task[10];
for (int i = 0; i < inc_tasks.Length; i++)
inc_tasks[i] = Task.Run(() =>
{
for (int loop = 1; loop <= 100; loop++)
MyMethod();
});
Task.WaitAll(inc_tasks);
Console.WriteLine("End of Main() with Value==" + TheResource.Value);
}
What I'm trying to achieve is to use the C# "using" statement at the top of a method (or somewhere in the middle, who cares) before the (exclusive) access to the shared resource and have the IDisposable mechanism automatically ending the exclusive access.
Under the hood the Monitor class is used for that.
The desired advantage is that no indented { code block } is required. Just a using... line and that's it. See MyMethod() in the sample above.
This seems to work quite fine. The final result of all inc's is as expected, even with long loops, and wrong if I remove the using.. statement from MyMethod.
However, do you think I can trust this solution? Is .Dispose of the token really, really always called when leaving MyMethod, even in case of an exception? Other pitfalls?
Thanks!