EventWaitHandle是否有任何隐式的MemoryBarrier?

时间:2009-03-25 13:09:26

标签: c# .net vb.net multithreading volatile

本网站的新用户,如果我没有以可接受的方式发帖,请告诉我。

我经常在下面的示例中对某些内容进行编码(为了清楚起见,请使用Dispose ommtited等内容)。我的问题是,如图所示需要挥发物吗?或者,当我读过Thread.Start时,ManualResetEvent.Set是否有隐式内存屏障?或者显式的MemoryBarrier调用是否比挥发性更强?还是完全错了?另外,据我所见,某些操作中的“隐式记忆障碍行为”没有记录,这是非常恐怖的事实,是否有某些操作的列表?

谢谢, 汤姆

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();

   }

}

5 个答案:

答案 0 :(得分:3)

volatile关键字不应该与使_a,_b和_c线程安全相混淆。有关更好的说明,请参阅here。此外,ManualResetEvent对_a,_b和_c的线程安全性没有任何影响。你必须单独管理它。

编辑:通过此编辑,我试图提取有关此问题的各种答案和评论中提供的所有信息。

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

暂时,让我们假设没有变量标记为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为杰弗里里希特的书所做的。我遇到的问题是EventWaitHandle用于同步线程,而不是访问数据。我从未见过使用EventWaitHandle(例如,ManualResetEvent)来同步数据访问的任何示例。因此,我很难相信EventWaitHandle会对内存障碍做任何事情。否则,我希望在互联网上找到 一些 引用。

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

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

  

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

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

在关闭此编辑之前,还有两点我想做。

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

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

感谢大家对此表示感谢。通过参与这次讨论,我确实学到了很多东西。

答案 1 :(得分:3)

请注意,这是关闭袖口,而不是密切研究您的代码。我不认为 Set执行内存障碍,但我不知道你的代码中的相关性如何?看起来更重要的是,如果Wait执行一个,它会这样做。因此,除非我在10秒内错过了一些用于查看代码的内容,否则我认为您不需要挥发性代码。

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

马特在评估方面做得很好,但他错过了一个细节。首先,让我们提供一些抛出的东西的定义,但这里没有说明。

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

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

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

我说,最初,我没有认为 Set执行了内存屏障。但是,在进一步思考里希特先生所说的内容后,Set将执行互锁操作,从而也可以确保缓存一致性。

我坚持原来的断言,这里不需要挥发性。

编辑2:看起来你正在构建一个“未来”。我建议你研究PFX,而不是自己动手。

答案 2 :(得分:3)

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

答案 3 :(得分:1)

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

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

由于编译器或emmpry optimaztions / reordering,你不希望在 Set()之后使所有线程可见_completed布尔值。同样,您不希望在Set()之后看到对结果_a,_b,_c的写入。

编辑:关于Matt Davis提到的项目的问题的进一步解释/澄清:

  

最后,我应该指出这一点   每当一个线程调用一个互锁的   方法,CPU强制缓存   一致性。所以,如果你在操纵   变量通过互锁方法,你   不必担心这一切   记忆模型的东西。此外,所有   线程同步锁(监视器,   ReaderWriterLock,Mutex,Semaphone,   AutoResetEvent,ManualResetEvent,   等)调用联锁方法   内部。

     

所以看起来像wekempf   指出,即结果变量   不需要volatile关键字   自从以后所示的例子   ManualResetEvent确保缓存   相干性。

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

但它是否阻止了重新保证,以便结果在完成标志之前分配,并且在设置了ManualResetEvent之前将完成标志分配为为真?

  

首先,我最初的假设是   后台线程会   可能会多次运行。一世   显然忽略了这个名字   class(OneUseBackgroundOp)!鉴于   它只运行一次,目前尚不清楚   为什么DoSomething()函数   以它的方式调用WaitOne()   确实。有什么等待的   initialWaitMs毫秒,如果   后台主题​​可能是也可能不是   在DoSomething()完成   回报?为什么不开始呢   后台线程并使用锁定   同步访问结果   变量或只是执行   Task()函数的内容为   调用的线程的一部分   做一点事()?有没有理由   这样做?

示例的概念是执行可能长期运行任务。如果任务可以在一段不可及的时间内完成,那么调用线程将获得对结果的访问权并继续正常处理。但是有时某个任务可能需要很长时间才能完成,并且在此期间无法阻止claiing线程,并且可以采取合理的步骤来解决这个问题。这可以包括稍后使用Completed属性检查操作。

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

  

其次,在我看来,不使用   某种锁定机制   结果变量仍然很糟糕   做法。没错,它不需要   代码如图所示。

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

  

但在某些时候   在路上,另一个线程可能会到来   沿着并尝试访问数据。它   在我的脑海里准备好会更好   为了这种可能性而不是   试图追查神秘的行为   异常后来。

因为我有一个易失性的已完成标志,保证在之前设置,但是对于结果的唯一访问是通过getter,并且如smaple中所提到的那样,异常如果调用getter并且操作尚未完成,则抛出,我希望可以通过调用DoSomething()之外的线程来调用Completed和result getter。无论如何,这是我的希望。无论如何,我相信这对挥发物来说也是如此。

答案 4 :(得分:0)

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

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

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

从您的问题来看,您似乎忽略了volatile所做的事情。所有volatile都告诉编译器该变量可能被其他线程异步修改,因此它不应该优化访问该变量的代码。 volatile不以任何方式同步对变量的访问。