KarloX
KarloX

Reputation: 987

Approach for synchronizing access to a resource in C# using the IDisposable pattern

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

Answers (2)

Servy
Servy

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

mikelegg
mikelegg

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

Related Questions