C#线程同步:Mutex/Monitor无法工作

3

我正在尝试只允许一个 Thread 访问一个非静态方法。我不会创建 Threads,而是使用 Tasks。我试图将该方法锁定为一个用途,但它并没有起作用,我首先尝试使用静态 Mutex 对象,然后是非静态的 Mutex 对象,但即使使用简单的监视器也不起作用。

我的代码:

private async void LoadLinkPreview()
    {
        try
        {
            // TODO: FEHLERTRÄCHTIG!
            Monitor.Enter(_loadLinkMonitor);
            Debug.WriteLine(DateTime.Now + ": Entered LinkPreview");

            // Code ...
            // There are also "await" usages here, don't know if this matters
        }
        catch { }
        finally
        {
             Monitor.Exit(_loadLinkMonitor);
             Debug.WriteLine(DateTime.Now + ": Left LinkPreview");
        }
    }

这是控制台显示的内容:

3/12/2013 6:10:03 PM: Entered LinkPreview
3/12/2013 6:10:05 PM: Entered LinkPreview
3/12/2013 6:10:05 PM: Left LinkPreview
3/12/2013 6:10:06 PM: Left LinkPreview

当然,有时它按照期望的顺序工作,但我希望它总是能够正常工作。也许有人可以帮助我吗?
编辑:我意识到控制台显示可能是期望的行为,因为“Left”的WriteLine()在退出监视器后,但现在我已经将它们切换了,并且可以确认肯定有两个线程同时工作!另外,控制台输出结果更加明显:
3/12/2013 6:26:15 PM: Entered LinkPreview
3/12/2013 6:26:15 PM: Entered LinkPreview
2 had an error! // Every time the method is used I count up now, this confirms that the second
// time the method is called the error is produced, which would not happen if the monitor
// is working because I have an if statement right upfront ...
3/12/2013 6:26:17 PM: Left LinkPreview
3/12/2013 6:26:18 PM: Left LinkPreview

那个内部的await可能很重要。不确定它是否适用于监视器,但如果线程尝试两次锁定(嵌套)某些东西,则允许它这样做而不被阻止。您会得到您所注意到的行为。 - Meirion Hughes
(如果同一个线程尝试两次锁定同一对象) - user7116
3个回答

4

4

实际上,你的代码可能已经按照预期运行了,问题只是在于显示上。

问题就出在这里:

 Monitor.Exit(_loadLinkMonitor);
 Debug.WriteLine(DateTime.Now + ": Left LinkPreview");

在退出监视器后,你需要写出时间戳。请注意,等待进入监视器的其他代码可以在调用Exit之后立即开始执行。因此,在当前线程调用exit之后,可能会出现另一个线程在Debug调用之前进入监视器并写下它自己的"进入"行。

如果你更改代码为:

 Debug.WriteLine(DateTime.Now + ": Left LinkPreview");
 Monitor.Exit(_loadLinkMonitor);

那么它不会改变行为,但是您将消除日志错误。如果仍然无序,则确实在代码中有问题。

另外,如果您有一个async方法,应避免调用执行阻塞等待的方法,例如Monitor.Enter。 如果您使用SemaphoreSlim,则可以使用其WaitAsync方法来确保该方法不会阻塞。示例代码如下:

private SemaphoreSlim semaphore = new SemaphoreSlim(1);
private async void LoadLinkPreview()
{
    try
    {
        await semaphore.WaitAsync();
        Debug.WriteLine(DateTime.Now + ": Entered LinkPreview");
        await Task.Delay(2000);//placeholder for real work/IO
    }
    finally
    {
        Debug.WriteLine(DateTime.Now + ": Left LinkPreview");
        semaphore.Release();
    }
}

嗨,谢谢。我已经编辑了我的答案,因为我发现有些地方不太清晰 =) 我会尝试使用 SemaphoreSlim,听起来很棒。但是它应该无论如何都能正常工作,对吧? - Smirnoff4u
@Smirnoff4u 第二部分,关于使用SemaphoreSlim并不涉及到你的问题,它只是你代码的一个改进。问题在于我在开头讨论的日志记录和锁定操作的顺序。 - Servy
是的,但我现在已经把它们交换了,它仍然无法工作,所以那不是问题。 - Smirnoff4u
@Smirnoff4u 那很可能与你没有展示的代码有关。 - Servy
SemaphoreSlim工作得非常好!非常感谢,我认为这也解释了我在其他应用程序中遇到的问题!一个后续问题:如果我需要一个互斥锁来进行进程同步,像这样的代码是否可以?public async static void F() { await _semaSlim.WaitAsync(); await DoMutexWork(); _semaSlim.Release(); }private static void MutexWork() { Mutex.WaitOne() // 存储相关操作 Mutex.ReleaseOne() } - Smirnoff4u
@Smirnoff4u 我认为是这样的。 - Servy

0

async方法中,您不能使用标准的多线程原语,例如Monitor。如果您使用lock语句,C#编译器将阻止您,但是如果您通过Monitor.Enter手动执行,则最终会自食其果。

如果您仔细思考一下,这是完全有道理的。当async方法产生时会发生什么?它会返回一个未完成的Task并允许其他代码运行,而它正在等待。这完全违反了锁的目的,即在持有锁时防止其他代码运行。还有一些情况(例如ASP.NET或线程池上下文),其中async方法在不同的线程上恢复;在这种情况下,一个线程获取锁,另一个线程释放锁。疯狂!猫和狗生活在一起!

内置的.NET解决方案是SemaphoreSlim,它作为锁/信号量工作(尽管它没有内置的IDisposable用于与using一起使用)。Stephen Toub在async协调原语上撰写了一系列博客,并且我在我的AsyncEx库中实现了一个完整套件,可通过NuGet获得。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接