EventWaitHandle是否有任何隐式的MemoryBarrier?

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

我经常在下面的示例中编写一些代码(为了清楚起见,使用Dispose ommtited等内容)。 我的问题是,如图所示需要挥发物吗? 或者,当我读过Thread.Start时,ManualResetEvent.Set是否有隐式内存屏障? 或者显式的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(); } } 

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

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

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

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

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

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

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

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

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

volatile关键字不应该与使_a,_b和_c线程安全相混淆。 请参阅此处以获得更好的解释。 此外,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就会强制缓存一致性。 因此,如果您通过互锁方法操作变量,则不必担心所有这些内存模型的内容。 此外,所有线程同步锁( MonitorReaderWriterLockMutexSemaphoneAutoResetEventManualResetEvent等)都在内部调用互锁方法。

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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