2

这与发出信号并立即关闭 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 对象首先被关闭了?

4

3 回答 3

3

这是我看到的:

  • 你得到ObjectDisposedException是因为你的代码表现出以下竞争条件:
    • 在所有线程都设法调用 flag.waitOne 之前,可以调用 flag.close。

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

这是一种方法:

如果所有已经启动的线程真的应该执行,你可以在调用 flag.close之前进行一些额外的同步。您可以通过使用StartNewonTask.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 异常,我认为已处理的对象没有为您执行内存屏障,您必须调用Thread.MemoryBarrier您的 catch 块确保已读取最新值。

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

祝你好运!

于 2012-02-17T10:36:29.447 回答
0

几点:

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

  2. 从内存屏障的角度来看,它是否等效几乎是无关紧要的——异常永远不应该是正常代码路径的一部分。我假设行为未定义,因为这不是预期的用例。

于 2012-02-17T04:13:38.587 回答
0

对我必须解决的实际问题进行更多思考,这比简化的示例要复杂一些,我决定使用 Monitor.Wait 和 Monitor.PulseAll。

Joe Albahari's Threading in C# proved extremely useful, in this particular case the following section applies: http://www.albahari.com/threading/part4.aspx#_Signaling_with_Wait_and_Pulse

于 2012-02-20T05:05:40.290 回答