简体   繁体   中英

Wrapping a Mutex with IDisposable and testing it but the test never ends

I'm trying to wrap a Mutex with an IDisposable class like this:

public class NamedMutex : IDisposable
{
    private static readonly object _syncLock = new object();
    private readonly Mutex _namedMutex;
    private readonly bool _createdNew;

    public NamedMutex(string name)
    {
        if (string.IsNullOrEmpty(name)) throw new ArgumentNullException("name");
        //lock (_syncLock)
        {
            _namedMutex = new Mutex(initiallyOwned: false, name: name, createdNew: out _createdNew);
        }
        _namedMutex.WaitOne();
    }

    public void Dispose()
    {
        //lock (_syncLock)
        {
            //if (_createdNew)
            _namedMutex.ReleaseMutex();
            _namedMutex.Dispose();
        }
    }
}

as you can see from the commented out code I've tried pretty much everything I could think of to make it work but either is my test wrong or something's not right with the above implementation because the test either never ends (probably a dead-lock that I am not able to identify or it crashes with the unsynchronized exception).

This is my test that I adapted for LINQPad:

void Main()
{
    var sw = Stopwatch.StartNew();

    var task1 = Task.Run(async () =>
    {
        using (new NamedMutex("foo"))
        {
            Console.WriteLine(3);
            await Task.Delay(TimeSpan.FromSeconds(3));
        }
    });

    var task2 = Task.Run(async () =>
    {
        using (new NamedMutex("foo"))
        {
            Console.WriteLine(2);
            await Task.Delay(TimeSpan.FromSeconds(2));
        }
    });

    Task.WaitAll(task1, task2);

    //Assert.IsTrue(sw.Elapsed.TotalSeconds >= 5);
    sw.Elapsed.Dump(); // LINQPad
}

This happens because of await . After your await Task.Delay(..) you might no longer be on the same thread you were before await statement. So in some cases you are trying to release your mutex from the thread which does not own it - hence your problem. That's easy to verify by writing current thread before and after await:

class Program {
    public static void Main() {
        while (true) {
            var sw = Stopwatch.StartNew();

            var task1 = Task.Run(async () => {                    
                using (new NamedMutex("foo")) {
                    Console.WriteLine("first before await: " + Thread.CurrentThread.ManagedThreadId);
                    await Task.Delay(TimeSpan.FromSeconds(2));
                    Console.WriteLine("first after await: " + Thread.CurrentThread.ManagedThreadId);
                }
            });

            var task2 = Task.Run(async () => {                    
                using (new NamedMutex("foo")) {
                    Console.WriteLine("second before await: " + Thread.CurrentThread.ManagedThreadId);
                    await Task.Delay(TimeSpan.FromSeconds(1));
                    Console.WriteLine("second after await: " + Thread.CurrentThread.ManagedThreadId);
                }
            });

            Task.WaitAll(task1, task2);

            //Assert.IsTrue(sw.Elapsed.TotalSeconds >= 5);
            Console.WriteLine(sw.Elapsed);
        }            
    }
}

To expand on Evk's answer , and to get to a workaround, it is still possible to wrap a Mutex with an IDisposable . You just have to make sure that you have complete control over the Thread that is acquiring the Mutex and releasing it, and you have to make sure that the context doesn't switch in that thread between acquiring and releasing the mutex.

So just spin up your own thread. Something like:

class NamedMutex : IDisposable
{
    private readonly Thread _thread;
    private readonly ManualResetEventSlim _disposalGate;
    private readonly Mutex _namedMutex;
    public NamedMutex(string name)
    {
        var constructorGate = new ManualResetEventSlim();
        _disposalGate = new ManualResetEventSlim();
        _thread = new Thread(() =>
        {
            // Code here to acquire the mutex
            _namedMutex = new Mutex(initiallyOwned: false, name: name, createdNew: out _createdNew);

            constructorGate.Set(); // Tell the constructor it can go on
            _disposalGate.Wait(); // Wait for .Dispose to be called

            // Code here to release the mutex
            _namedMutex.ReleaseMutex();
            _namedMutex.Dispose();
        });
        _thread.Start();
        constructorGate.Wait();
    }

    public void Dispose()
    {
        _disposalGate.Set();
    }
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM