Reputation: 987
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!
Upvotes: 0
Views: 84
Reputation: 203802
Your code has the potential to hold onto the lock outside of the scope of the critical section if an exception is thrown after you enter the monitor in WantAccess and before the returned value is assigned to the variable in the using
block. lock
has no potential for doing the same due to the way the transformation is done.
lock
is specially designed for exactly this problem, and is very finely tuned to do exactly what you would want it to do when solving this kind of problem. You should use the right tool for the job.
Upvotes: 1
Reputation: 1327
I don't see anything wrong with this at all. I have used the using pattern in a similar way before without issue.
As a simple example, consider the pattern for using a database connection. Underneath there is a connection pool, creating a new connection takes from the pool (possibly waiting) and disposing releases back to the pool. This is the same as you have, all be it with a pool of 1 item.
I don't know if the effort is worth not just using a simple lock(){} pattern if the resource is only shared within 1 process. That would be more 'conventional'. But only you can answer that.
Upvotes: 0