EventWaitHandle 是否有任何隐式 MemoryBarrier?

发布于 2024-07-15 04:58:41 字数 1664 浏览 5 评论 0原文

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

我经常按照下面的示例编写一些代码(为了清楚起见,省略了诸如 Dispose 之类的内容。)。 我的问题是,是否需要如图所示的挥发物? 或者 ManualResetEvent.Set 是否像我读过的 Thread.Start 那样具有隐式内存屏障? 或者显式的 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();

   }

}

New to this website, so let me know if I'm not posting in an accepted manner.

I've frequently coded something along the lines of the sample below(with stuff like Dispose ommited for clarity. ). My question is, are the volatiles needed as shown? Or does the ManualResetEvent.Set have an implicit memory barrier as I've read Thread.Start does? Or would an explicit MemoryBarrier call be better than the volatiles? Or is it completely wrong? Also, the fact that the "implicit memory barrier behavior" in some operations is not documented as far as I've seen is quite frutrating, is there a list of these operations somewhere?

Thanks,
Tom

:

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

   }

}

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(5

何止钟意 2024-07-22 04:58:41

请注意,这是即兴的,没有仔细研究您的代码。 我不认为 Set 会执行内存屏障,但我不明白这与您的代码有什么关系? 似乎更重要的是 Wait 是否执行一个操作,它确实执行了。 因此,除非我在花 10 秒查看您的代码时错过了某些内容,否则我不认为您需要 挥发物。

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

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

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

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

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

我一开始就说过,我认为 Set 并没有执行内存屏障。 然而,进一步思考 Richter 先生所说的,Set 将执行互锁操作,因此也将确保缓存一致性。

我坚持我最初的主张,即这里不需要 volatile。

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

Note that this is off the cuff, without studying your code closely. I don't think Set performs a memory barrier, but I don't see how that's relevant in your code? Seems like more important would be if Wait performs one, which it does. So unless I missed something in the 10 seconds I devoted to looking at your code, I don't believe you need the volatiles.

Edit: Comments are too restrictive. I'm now referring to Matt's edit.

Matt did a good job with his evaluation, but he's missing a detail. First, let's provide some definitions of things thrown around, but not clarified here.

A volatile read reads a value and then invalidates the CPU cache. A volatile write flushes the cache, and then writes the value. A memory barrier flushes the cache and then invalidates it.

The .NET memory model ensures that all writes are volatile. Reads, by default, are not, unless an explicit VolatileRead is made, or the volatile keyword is specified on the field. Further, interlocked methods force cache coherency, and all of the synchronization concepts (Monitor, ReaderWriterLock, Mutex, Semaphore, AutoResetEvent, ManualResetEvent, etc.) call interlocked methods internally, and thus ensure cache coherency.

Again, all of this is from Jeffrey Richter's book, "CLR via C#".

I said, initially, that I didn't think Set performed a memory barrier. However, upon further reflection about what Mr. Richter said, Set would be performing an interlocked operation, and would thus also ensure cache coherency.

I stand by my original assertion that volatile is not needed here.

Edit 2: It looks as if you're building a "future". I'd suggest you look into PFX, rather than rolling your own.

岁月流歌 2024-07-22 04:58:41

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

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

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

暂时,我们假设没有任何变量被标记为易失性的。 在这种情况下,可以在 Task() 中设置标志变量之后设置结果变量,如下所示:

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

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

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

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

   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 都会强制缓存一致性。 因此,如果您通过互锁方法操作变量,则不必担心所有这些内存模型内容。 此外,所有线程同步锁(MonitorReaderWriterLockMutexSemaphoneAutoResetEvent、ManualResetEvent等)在内部调用互锁方法。


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

在结束本次编辑之前,我想补充两点。

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

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

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

The volatile keyword should not confused as to making _a, _b, and _c thread-safe. See here for a better explanation. Further, the ManualResetEvent does not have any bearing on the thread-safety of _a, _b, and _c. You have to manage that separately.

EDIT: With this edit, I am attempting to distill all of the information that has been put in various answers and comments regarding this question.

The basic question is whether or not the result variables (_a, _b, and _c) will be 'visible' at the time the flag variable (_completed) returns true.

For a moment, let's assume that none of the variables are marked volatile. In this case, it would be possible for the result variables to be set after the flag variable is set in Task(), like this:

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

This is clearly not what we want, so how do we deal with this?

If these variables are marked volatile, then this reordering will be prevented. But that is what prompted the original question - are the volatiles required or does the ManualResetEvent provide an implicit memory barrier such that reordering does not occur, in which case the volatile keyword is not really necessary?

If I understand correctly, wekempf's position is that the WaitOne() function provides an implicit memory barrier which fixes the problem. BUT that doesn't seem sufficient to me. The main and background threads could be executing on two separate processors. So, if Set() does not also provide an implicit memory barrier, then the Task() function could end up being executed like this on one of the processors (even with the volatile variables):

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

I have searched high and low for information regarding memory barriers and the EventWaitHandles, and I have come up with nothing. The only reference I have seen is the one wekempf has made to Jeffrey Richter's book. The problem I have with this is that the EventWaitHandle is meant to synchronize threads, not access to data. I have never seen any example where EventWaitHandle (e.g., ManualResetEvent) is used to synchronize access to data. As such, I'm hard-pressed to believe that EventWaitHandle does anything with regard to memory barriers. Otherwise, I would expect to find some reference to this on the internet.

EDIT #2: This is a response to wekempf's response to my response... ;)

I managed to read the section from Jeffrey Richter's book at amazon.com. From page 628 (wekempf quotes this too):

Finally, i should point out that whenever a thread calls an interlocked method, the CPU forces cache coherency. So if you are manipulating variables via interlocked methods, you do not have to worry about all of this memory model stuff. Furthermore, all thread synchronization locks (Monitor, ReaderWriterLock, Mutex, Semaphone, AutoResetEvent, ManualResetEvent, etc.) call interlocked methods internally.

So it would seem that, as wekempf pointed out, that the result variables do not require the volatile keyword in the example as shown since the ManualResetEvent ensures cache coherency.

Before closing this edit, there are two additional points I'd like to make.

First, my initial assumption was that the background thread would potentially run multiple times. I obviously overlooked the name of the class (OneUseBackgroundOp)! Given that it is only run once, it is not clear to me why the DoSomething() function calls WaitOne() in the manner that it does. What is the point of waiting initialWaitMs milliseconds if the background thread may or may not be done at the time DoSomething() returns? Why not just kickoff the background thread and use a lock to synchronize access to the results variables OR simply execute the contents of the Task() function as part of the thread that calls DoSomething()? Is there a reason not to do this?

Second, it seems to me that not using some kind of locking mechanism on the results variables is still a bad approach. True, it is not needed in the code as shown. But at some point down the road, another thread may come along and try to access the data. It would be better in my mind to prepare for this possibility now rather than try to track down mysterious behavior anomalies later.

Thanks to everyone for bearing with me on this. I've certainly learned a lot by participating in this discussion.

苹果你个爱泡泡 2024-07-22 04:58:41

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

Wait functions have an implicit memory barrier. See http://msdn.microsoft.com/en-us/library/ms686355(v=vs.85).aspx

雨轻弹 2024-07-22 04:58:41

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

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

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

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

最后我要指出的是
每当线程调用互锁时
方法,CPU强制缓存
连贯性。 所以如果你正在操纵
通过互锁方法的变量,您
不必担心这一切
内存模型的东西。 此外,所有
线程同步锁(Monitor,
ReaderWriterLock、互斥体、信号音、
自动重置事件、手动重置事件、
等)调用互锁方法
内部。

所以看起来,正如 wekempf
指出,结果变量
不需要 volatile 关键字
自以下所示的示例
ManualResetEvent 确保缓存
一致性。

因此,你们都同意这样的操作负责处理器之间或寄存器等中的缓存。

但是它是否可以防止重新排序以保证结果都在完成标志之前分配,并且在设置 ManualResetEvent 之前,已完成标志被指定为 true 吗?

首先,我最初的假设是
后台线程会
可能会运行多次。 我
显然忽略了名字
类(OneUseBackgroundOp)! 鉴于
它只运行一次,尚不清楚
对我来说为什么 DoSomething() 函数
以如下方式调用 WaitOne()
做。 等待有什么意义
initialWaitMs 毫秒,如果
后台线程可能是也可能不是
当时完成的 DoSomething()
回报? 为什么不直接开始
后台线程并使用锁
同步访问结果
变量或简单地执行
Task() 函数的内容为
调用线程的一部分
做一点事()? 有什么理由不
来做到这一点?

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

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

其次,在我看来,不使用
某种锁定机制
结果变量还是不好
方法。 确实,它不需要在
代码如图所示。

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

但是在某个时候
沿着这条路,另一个线程可能会出现
并尝试访问数据。 它
我认为最好做好准备
现在就寻找这种可能性而不是
尝试追踪神秘行为
稍后出现异常。

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

First, I'm not sure if I should "Answer my own question" or use a comment for this, but here goes:

My understanding is that volatile prevents code/memory optimizations from moving the accesses to my result variables (and the completed boolean) such that the the thread that reads the result will see upt-to-date data.

You wouldn't want the _completed boolean made visible to all threads after the Set() due to compiler or emmpry optimaztions/reordering. Likewise, you wouldn't want the writes to the results _a, _b, _c being seen after the Set().

EDIT: Further explaination/clarification on the question, in regards to items mentioned by Matt Davis:

Finally, i should point out that
whenever a thread calls an interlocked
method, the CPU forces cache
coherency. So if you are manipulating
variables via interlocked methods, you
do not have to worry about all of this
memory model stuff. Furthermore, all
thread synchronization locks (Monitor,
ReaderWriterLock, Mutex, Semaphone,
AutoResetEvent, ManualResetEvent,
etc.) call interlocked methods
internally.

So it would seem that, as wekempf
pointed out, that the result variables
do not require the volatile keyword in
the example as shown since the
ManualResetEvent ensures cache
coherency.

So you are both in agreement that such an operation takes care of caching between processors or in registers etc.

But does it prevent reording to guarantee such that BOTH the results are assigned before the completed flag, and that the completed flag is assigned true before the ManualResetEvent is Set?

First, my initial assumption was that
the background thread would
potentially run multiple times. I
obviously overlooked the name of the
class (OneUseBackgroundOp)! Given that
it is only run once, it is not clear
to me why the DoSomething() function
calls WaitOne() in the manner that it
does. What is the point of waiting
initialWaitMs milliseconds if the
background thread may or may not be
done at the time DoSomething()
returns? Why not just kickoff the
background thread and use a lock to
synchronize access to the results
variables OR simply execute the
contents of the Task() function as
part of the thread that calls
DoSomething()? Is there a reason not
to do this?

The concept of the sample is to execute a possibly long-runnig task. If the task can be completed within an exceptable amount of time, then the calling thread will get access to the result and continue with normal processing. But sometime a task can take quite a long time and the claiing thread cannot be blocked for that period and can take reasonable steps to deal with that. That can include checking back later on the operation using the Completed property.

A concrete example: A DNS resolve is often very quick (subsecond) and worth waiting for even from a GUI, but sometimes it can take many many seconds. So by using a utility class like the sample, one could gets a result easily from the point-of-view of the caller 95% of the time and not lock up the GUI the other 5%. One could use a Background worker, but that can be overkill for an operation that the vast majority of the time doesn't need all that plumbing.

Second, it seems to me that not using
some kind of locking mechanism on the
results variables is still a bad
approach. True, it is not needed in
the code as shown.

The result (and completed flag) data is meant to be write-once, read-many. If I added a lock to assign the results and flag, I'd also have to lock in my result getters, and I never liked seeing getters lock just to return a data point. From my reading, such fine-grained locking is not appropriate. If an operation has 5 or 6 results, the caller has to take and release the lock 5 or 6 times needlessly.

But at some point
down the road, another thread may come
along and try to access the data. It
would be better in my mind to prepare
for this possibility now rather than
try to track down mysterious behavior
anomalies later.

Because I have a volatile completed flag that is guarenteed to be set before the volatile results are, and the only access to the results is through the getters, and as mentioned in the smaple, an exception is thrown if the getter is called and the operation is not yet complete, I'd expect that the Completed and result getters CAN be invoked by a thread other than the one that called DoSomething(). That's my hope anyway. I believe this to be true with the volatiles anyway.

£冰雨忧蓝° 2024-07-22 04:58:41

根据您所展示的内容,我想说,不,该代码中不需要 易失性

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

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

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

Based on what you've shown, I would say that, no, the volatiles are not required in that code.

The ManualResetEvent itself doesn't have an implicit memory barrier. However, the fact that the main thread is waiting for the signal means that it can't modify any variables. At least, it can't modify any variables while it's waiting. So I guess you could say that waiting on a synchronization object is an implicit memory barrier.

Note, however, that other threads, if they exist and have access to those variables, could modify them.

From your question, it seems that you're missing the point of what volatile does. All volatile does is tell the compiler that the variable might be modified by other threads asynchronously, so it shouldn't optimize code that accesses the variable. volatile does not in any way synchronize access to the variable.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文