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

假设我有一堆可能要执行相同工作的线程,但只应允许一个线程执行,而其他线程则应等到工作人员完成并使用其结果。

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

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

我修改了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");
}

我想我的问题的核心是:

就内存障碍而言,是否因ObjectDisposedException而中止对WaitOne的调用,是否等效于成功对WaitOne的调用?

那应该确保其他线程安全地访问变量workResult。

我的猜测:这必须是安全的,否则WaitOne如何安全地确定ManualResetEvent对象首先已关闭?

最佳答案

这是我看到的:

  • 您正在获取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变量将是最新的变量,而无需任何其他的内存屏障或 volatile 读取。

    因此,要回答您的问题,如果您必须避免额外的同步并通过使用方法来处理ObjectDisposed异常,我会认为已处置的对象并未为您执行内存障碍,则您必须在catch块中调用 Thread.MemoryBarrier 以确保已读取最新值。

    但是异常的代价很高,如果您可以避免在正常的程序执行中使用它们,我相信这样做是明智的。

    祝你好运!

    10-05 17:53