10

刚接触这个网站,所以如果我没有以可接受的方式发帖,请告诉我。

我经常按照下面的示例编写一些代码(为了清楚起见,省略了 Dispose 之类的东西。)。我的问题是,是否需要如图所示的挥发物?或者 ManualResetEvent.Set 是否像我读过的那样具有隐式内存屏障 Thread.Start 呢?或者明确的 MemoryBarrier 调用会比 volatile 更好吗?还是完全错误?此外,据我所见,某些操作中的“隐式内存屏障行为”没有记录,这很令人沮丧,在某处是否有这些操作的列表?

谢谢,汤姆

class OneUseBackgroundOp
{

   // background args
   private string _x;
   private object _y;
   private long _z;

   // background results
   private volatile DateTime _a
   private volatile double _b;
   private volatile object _c;

   // thread control
   private Thread _task;
   private ManualResetEvent _completedSignal;
   private volatile bool _completed;

   public bool DoSomething(string x, object y, long z, int initialWaitMs)
   {
      bool doneWithinWait;

      _x = x;
      _y = y;
      _z = z;

      _completedSignal = new ManualResetEvent(false);

      _task = new Thread(new ThreadStart(Task));
      _task.IsBackground = true;
      _task.Start()

      doneWithinWait = _completedSignal.WaitOne(initialWaitMs);

      return doneWithinWait;

   }

   public bool Completed
   {
      get
      {
         return _completed;
      }
   }

   /* public getters for the result fields go here, with an exception
      thrown if _completed is not true; */

   private void Task()
   {
      // args x, y, and z are written once, before the Thread.Start
      //    implicit memory barrier so they may be accessed freely.

      // possibly long-running work goes here

      // with the work completed, assign the result fields _a, _b, _c here

      _completed = true;
      _completedSignal.Set();

   }

}
4

5 回答 5

3

volatile 关键字不应混淆为使 _a、_b 和 _c 线程安全。请参阅此处以获得更好的解释。此外,ManualResetEvent 对 _a、_b 和 _c 的线程安全没有任何影响。您必须单独管理它。

编辑:通过这个编辑,我试图提炼所有关于这个问题的答案和评论中的信息。

基本问题是结果变量(_a、_b 和 _c)在标志变量(_completed)返回 true 时是否“可见”。

暂时,让我们假设没有一个变量被标记为 volatile。在这种情况下,可以在 Task() 中设置标志变量之后设置结果变量,如下所示:

   private void Task()
   {
      // possibly long-running work goes here
      _completed = true;
      _a = result1;
      _b = result2;
      _c = result3;
      _completedSignal.Set();
   }

这显然不是我们想要的,那么我们该如何处理呢?

如果这些变量被标记为易失性,那么这种重新排序将被阻止。但这就是引发原始问题的原因 -是否需要 volatile 或者 ManualResetEvent 是否提供了隐式内存屏障,以便不会发生重新排序,在这种情况下, volatile 关键字真的不需要?

如果我理解正确,wekempf 的立场是 WaitOne() 函数提供了一个隐式内存屏障来解决问题。 但这对我来说似乎还不够。主线程和后台线程可以在两个单独的处理器上执行。因此,如果 Set() 也没有提供隐式内存屏障,则 Task() 函数最终可能会在其中一个处理器上像这样执行(即使使用 volatile 变量):

   private void Task()
   {
      // possibly long-running work goes here
      _completedSignal.Set();
      _a = result1;
      _b = result2;
      _c = result3;
      _completed = true;
   }

我搜索了有关内存屏障和 EventWaitHandles 的信息,但一无所获。我看到的唯一参考资料是 wekempf 对 Jeffrey Richter 的书所做的参考资料。我遇到的问题是 EventWaitHandle 旨在同步线程,而不是访问数据。我从未见过任何使用EventWaitHandle(例如ManualResetEvent)来同步数据访问的例子。因此,我很难相信 EventWaitHandle 在内存屏障方面做了任何事情。否则,我希望在互联网上找到一些对此的参考。

编辑#2:这是对 wekempf 对我的回应的回应...... ;)

我设法在 amazon.com 上阅读了 Jeffrey Richter 书中的部分。从第 628 页开始(wekempf 也引用了这一点):

最后,我应该指出,每当线程调用互锁方法时,CPU 都会强制缓存一致性。因此,如果您通过互锁方法操作变量,则不必担心所有这些内存模型的东西。此外,所有线程同步锁(MonitorReaderWriterLockMutexSemaphoneAutoResetEventManualResetEvent等)在内部调用互锁方法。

因此,正如 wekempf 所指出的,结果变量似乎不需要示例中的 volatile 关键字,因为 ManualResetEvent 确保了缓存的一致性。

在结束此编辑之前,我还想补充两点。

首先,我最初的假设是后台线程可能会运行多次。我显然忽略了类的名称(OneUseBackgroundOp)!鉴于它只运行一次,我不清楚为什么 DoSomething() 函数会以这种方式调用 WaitOne() 。如果后台线程在 DoSomething() 返回时可能完成也可能不完成,那么等待 initialWaitMs 毫秒有什么意义?为什么不直接启动后台线程并使用锁来同步对结果变量的访问,或者只是将 Task() 函数的内容作为调用 DoSomething() 的线程的一部分执行?有理由不这样做吗?

其次,在我看来,不对结果变量使用某种锁定机制仍然是一种不好的方法。诚然,代码中不需要它,如图所示。但是在某个时候,另一个线程可能会出现并尝试访问数据。在我看来,最好现在就为这种可能性做好准备,而不是以后试图追查神秘的行为异常。

感谢大家对我的支持。通过参与这次讨论,我当然学到了很多东西。

于 2009-03-25T14:37:37.970 回答
3

请注意,这是即兴的,无需仔细研究您的代码。我不认为Set 执行内存屏障,但我看不出这与您的代码有什么关系?似乎更重要的是 Wait 执行一个,它确实执行了。因此,除非我在专用于查看您的代码的 10 秒内错过了某些内容,否则我认为您不需要 volatiles。

编辑:评论太严格了。我现在指的是马特的编辑。

马特的评估做得很好,但他遗漏了一个细节。首先,让我们提供一些关于抛出的东西的定义,但这里没有澄清。

易失性读取读取一个值,然后使 CPU 缓存无效。易失性写入会刷新缓存,然后写入值。内存屏障刷新缓存,然后使其无效。

.NET 内存模型确保所有写入都是易失的。默认情况下,不会读取,除非进行了显式 VolatileRead,或者在字段上指定了 volatile 关键字。此外,interlocked 方法强制缓存一致性,所有同步概念(Monitor、ReaderWriterLock、Mutex、Semaphore、AutoResetEvent、ManualResetEvent 等)在内部调用 interlocked 方法,从而确保缓存一致性。

同样,所有这些都来自 Jeffrey Richter 的书“CLR via C#”。

我说,最初,我不认为Set 执行了内存屏障。然而,在进一步思考 Richter 先生所说的之后,Set 将执行一个互锁操作,因此也将确保缓存的一致性。

我坚持我最初的断言,即这里不需要 volatile。

编辑 2:看起来你正在建立一个“未来”。我建议您查看PFX,而不是自己滚动。

于 2009-03-25T18:09:21.643 回答
3

等待函数具有隐式内存屏障。请参阅http://msdn.microsoft.com/en-us/library/ms686355(v=vs.85).aspx

于 2012-05-18T11:15:31.740 回答
1

首先,我不确定我是否应该“回答我自己的问题”或为此使用评论,但这里是:

我的理解是 volatile 阻止代码/内存优化移动对我的结果变量(和完成的布尔值)的访问,以便读取结果的线程将看到最新数据。

由于编译器或 emmpry 优化/重新排序,您不希望 _completed 布尔值在 Set()之后对所有线程可见。同样,您不希望在 Set() 之后看到对结果 _a、_b、_c 的写入。

编辑:关于问题的进一步解释/澄清,关于马特戴维斯提到的项目:

最后,我应该指出,每当线程调用互锁方法时,CPU 都会强制缓存一致性。因此,如果您通过互锁方法操作变量,则不必担心所有这些内存模型的东西。此外,所有线程同步锁(Monitor、ReaderWriterLock、Mutex、Semaphone、AutoResetEvent、ManualResetEvent 等)在内部调用互锁方法。

因此,正如 wekempf 所指出的,结果变量似乎不需要示例中的 volatile 关键字,因为 ManualResetEvent 确保了缓存的一致性。

所以你们都同意这样的操作负责处理器之间或寄存器等的缓存。

但是它是否会阻止重新排序以保证在完成标志之前分配两个结果并且在设置 ManualResetEvent 之前将完成标志分配为真?

首先,我最初的假设是后台线程可能会运行多次。我显然忽略了类的名称(OneUseBackgroundOp)!鉴于它只运行一次,我不清楚为什么 DoSomething() 函数会以这种方式调用 WaitOne() 。如果后台线程在 DoSomething() 返回时可能完成也可能不完成,那么等待 initialWaitMs 毫秒有什么意义?为什么不直接启动后台线程并使用锁来同步对结果变量的访问,或者只是将 Task() 函数的内容作为调用 DoSomething() 的线程的一部分执行?有理由不这样做吗?

样本的概念是执行一个可能长时间运行的任务。如果该任务可以在一个例外的时间内完成,那么调用线程将可以访问结果并继续正常处理。但有时一个任务可能需要相当长的时间,并且在此期间不能阻塞调用线程,并且可以采取合理的步骤来处理它。这可以包括稍后使用 Completed 属性检查操作。

一个具体的例子:DNS 解析通常非常快(亚秒级),即使从 GUI 也值得等待,但有时可能需要很多秒。因此,通过使用像示例这样的实用程序类,可以在 95% 的时间里从调用者的角度轻松获得结果,而在另外 5% 的时间里不会锁定 GUI。可以使用后台工作人员,但这对于绝大多数时间不需要所有管道的操作来说可能是多余的。

其次,在我看来,不对结果变量使用某种锁定机制仍然是一种不好的方法。诚然,代码中不需要它,如图所示。

结果(和完成标志)数据是一次写入,多次读取。如果我添加了一个锁来分配结果和标志,我还必须锁定我的结果 getter,而且我从不喜欢看到 getter 锁定只是为了返回一个数据点。从我的阅读来看,这种细粒度的锁定是不合适的。如果一个操作有 5 或 6 个结果,调用者必须不必要地获取和释放锁 5 或 6 次。

但是在某个时候,另一个线程可能会出现并尝试访问数据。在我看来,最好现在就为这种可能性做好准备,而不是以后试图追查神秘的行为异常。

因为我有一个 volatile 已完成标志,保证在 volatile 结果之前设置,并且对结果的唯一访问是通过 getter,并且如 smaple 中所述,如果调用 getter 并且操作将引发异常尚未完成,我希望 Completed 和结果 getter 可以由调用 DoSomething() 的线程以外的线程调用。无论如何,这就是我的希望。无论如何,我相信这对于挥发物来说是正确的。

于 2009-03-25T19:49:34.040 回答
0

根据您所展示的内容,我会说,不,volatiles该代码中不需要。

ManualResetEvent本身没有隐式内存屏障。然而,主线程正在等待信号的事实意味着它不能修改任何变量。至少,它在等待时不能修改任何变量。所以我想你可以说等待同步对象是一个隐式内存屏障。

但是请注意,如果其他线程存在并且可以访问这些变量,则可以修改它们。

从你的问题来看,你似乎错过了做什么volatile。所做volatile的只是告诉编译器该变量可能被其他线程异步修改,因此它不应该优化访问该变量的代码。 volatile不会以任何方式同步对变量的访问。

于 2009-03-25T14:32:24.457 回答