【问题标题】:Wrapping a Mutex with IDisposable and testing it but the test never ends用 IDisposable 包装 Mutex 并对其进行测试,但测试永远不会结束
【发布时间】:2017-03-09 23:59:32
【问题描述】:

我正在尝试用这样的 IDisposable 类包装 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
}

【问题讨论】:

  • 你能在NamedMutex 的构造函数中放置一个断点,并告诉我们它是否超过了_namedMutex.WaitOne(); 行吗?
  • @MattThomas 是的。如果我在它下面添加Console.WriteLine("WaitOne");,那么它只打印一次WaitOne,然后立即打印3,它会永远挂起。
  • 我刚刚在visual studio中测试了这个,第一次可以正常工作,但是所有后续尝试都失败了。然后,如果您更改互斥锁的名称,它会再次工作一次,然后在后续调用中再次失败。程序结束后似乎没有正确处理互斥锁。
  • 另外,如果您在后续调用中等待的时间足够长,Task.WaitAll 会抛出一个 AggregateException,其中包含一个 ApplicationException,上面写着“对象同步方法是从一个未同步的代码块中调用的。”
  • 我想知道它是否与asyncawait有关...documentation谈到在与获得它的线程相同的线程上发布它,我认为awaiting某些东西会导致随后出现的代码在不同的线程上执行?编辑:哇! Evk 打败了我

标签: c# thread-safety mutex idisposable


【解决方案1】:

这是因为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);
        }            
    }
}

【讨论】:

  • 我才意识到这一点,然后来回答,你打败了我。
  • 哦,所以这可能意味着我可以忘记将Mutexasync/await 一起使用?
  • 另外,请注意,这取决于当前的SynchronizationContext,在 UI 应用程序(例如 WinForms 或 WPF)中,不会发生此问题,因为它们的上下文会将延续编组到 UI 线程,但是控制台应用程序没有要编组的“主”线程,因此延续只是在任意 ThreadPool 线程上运行。
  • @t3chb0t 是的。您可能已经注意到,您不能在“lock”语句中执行 async\await,现在您可以了解原因了 - 因为这样做是一个非常糟糕的主意。
  • @t3chb0t 如果您需要某种异步同步,您可以使用SemaphoreSlim,它有一个 WaitAsync 方法。有一个关于它的博客here
【解决方案2】:

要扩展Evk's answer,并找到解决方法,仍然可以用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();
    }
}

【讨论】:

  • 请参考我的comment。我需要这个名称,因为它标识了当前两个进程无法同时修改的资源。
  • @t3chb0t 对。这只是要点。因此,通过NamedMutex 构造函数输入名称并获取// Code here to acquire the mutex 注释所在的命名互斥体。我对其进行了编辑以添加这些内容
  • 这似乎可行,虽然它确实涉及每次创建一个新线程,这并不便宜。
  • @t3chb0t 是的,不便宜,不幸的是
  • 好吧,也许你是对的,但对此并不完全确定,但没有任何论据:)
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2017-01-02
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-08-05
  • 2021-08-08
  • 1970-01-01
相关资源
最近更新 更多