在ManualResetEvent.WaitOne()上捕获ObjectDisposedException是否安全?

时间:2012-02-17 03:56:52

标签: c# .net multithreading manualresetevent

这与Is it safe to signal and immediately close a ManualResetEvent?密切相关,可能会为该问题提供一种解决方案。

假设我有一堆线程可能想要做同样的工作,但只允许一个人做这件事,其他人应该等到工人完成并使用它的结果。

所以我基本上只希望工作一次。

更新: 让我补充一点,这不是可以使用.net 4的Lazy< T>解决的初始化问题。我的意思是每个任务一次,这些任务是在运行时确定的。从下面的简化示例中可能不太清楚这一点。

修改Hans Passant对上述问题的回答的简单例子,我想以下是安全的。 (它与刚刚描述的用例略有不同,但就线程及其关系而言,它是等效的)

static void Main(string[] args)
{
    ManualResetEvent flag = new ManualResetEvent(false);
    object workResult = null;
    for (int ix = 0; ix < 10; ++ix)
    {
        ThreadPool.QueueUserWorkItem(s =>
        {
            try
            {
                flag.WaitOne();
                Console.WriteLine("Work Item Executed: {0}", workResult);
            }
            catch (ObjectDisposedException)
            {
                Console.WriteLine("Finished before WaitOne: {0}", workResult);
            }
        });
    }
    Thread.Sleep(1000);
    workResult = "asdf";
    flag.Set();
    flag.Close();
    Console.WriteLine("Finished");
}

我想我的问题的核心是:

对WaitOne的调用是否因ObjectDisposedException而中止,相当于在内存障碍方面成功调用WaitOne?

这应确保其他线程安全访问变量 workResult

我的猜测:它必须是安全的,否则WaitOne怎么能安全地发现ManualResetEvent对象首先被关闭了?

3 个答案:

答案 0 :(得分:3)

以下是我看到的内容:

  • 您收到ObjectDisposedException,因为您的代码显示以下竞争条件:
    • 可以在所有线程都设法调用flag.waitOne之前调用flag.close。

如何处理这取决于在flag.waitOne之后执行代码的重要性。

这是一种方法:

如果所有已经启动的线程都应该执行,那么在调用flag.close之前,您可以进行一些额外的同步。您可以使用StartNew上的Task.Factory代替Thread.QueueUserWorkItem来实现此目的。任务可以等待完成,然后你将调用flag.close从而消除竞争条件和处理ObjectDisposedException的需要

您的代码将成为:

    static void Main(string[] args)
    {
        ManualResetEvent flag = new ManualResetEvent(false);
        object workResult = null;
        Task[] myTasks = new Task[10];
        for (int ix = 0; ix < myTasks.Length; ++ix)
        {
            myTasks[ix] = Task.Factory.StartNew(() =>
            {
                flag.WaitOne();
                Console.WriteLine("Work Item Executed: {0}", workResult);
            });
        }

        Thread.Sleep(1000);
        workResult = "asdf";
        flag.Set();
        Task.WaitAll(); // Eliminates race condition
        flag.Close();
        Console.WriteLine("Finished");
    }

正如您在上面所看到的,任务允许额外的同步,以消除您所看到的竞争条件。

作为额外注释,ManualResetEvent.waitOne执行内存屏障,因此workresult变量将是最新更新的,不需要任何进一步的内存屏障或易失性读取。

所以要回答你的问题,如果你真的必须避免额外的同步并通过你的方法处理一个ObjectDisposed异常,我会争辩说,一个被处置的对象没有为你执行内存屏障,您必须在catch块中调用Thread.MemoryBarrier以确保已读取最新值。

但是异常是昂贵的,如果你可以避免它们用于正常的程序执行,我相信这样做是明智的。

祝你好运!

答案 1 :(得分:0)

几点:

  1. 如果这是.NET 4,那么Lazy是更好的方法。

  2. 从内存障碍的角度来看,它是否相同是无关紧要的 - 例外不应该是正常代码路径的一部分。我认为行为是未定义的,因为这不是预期的用例。

答案 2 :(得分:0)

对于我必须解决的实际问题反映了一点,这比简化示例复杂一点,我决定使用Monitor.Wait和Monitor.PulseAll。

Joe Albahari的 Cread中的线程证明非常有用,在这种特殊情况下,以下部分适用:http://www.albahari.com/threading/part4.aspx#_Signaling_with_Wait_and_Pulse