用 IDisposable 包装互斥锁并测试它,但测试永远不会结束

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

我正在尝试用 IDisposable class 包裹 Mutex,如下所示:

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();
        }
    }
}

正如您从注释掉的代码中看到的那样,我已经尝试了几乎所有我能想到的方法来使其工作,但要么是我的测试错误,要么是上述实现不正确,因为测试永远不会结束(可能是我无法识别的死锁,或者它因未同步异常而崩溃)。

这是我为 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
}

这是因为 await。在您的 await Task.Delay(..) 之后,您可能不再处于与 await 声明之前相同的线程中。因此,在某些情况下,您试图从不拥有它的线程中释放您的互斥量 - 因此您的问题。这很容易通过在 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);
        }            
    }
}

要扩展 ,并找到解决方法,仍然可以用 IDisposable 包裹 Mutex。您只需要确保您对获取 Mutex 并释放它的 Thread 具有完全控制权,并且您必须确保上下文不会在该线程中在获取和释放之间切换释放互斥量。

因此,只需启动您自己的线程即可。类似于:

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();
    }
}