EventWaitHandle 是否有任何隐式 MemoryBarrier?
我是这个网站的新手,所以如果我没有以可接受的方式发帖,请告诉我。
我经常按照下面的示例编写一些代码(为了清楚起见,省略了诸如 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 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(5)
请注意,这是即兴的,没有仔细研究您的代码。 我不认为 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.
不应该将 volatile 关键字与使 _a、_b 和 _c 线程安全混淆。 请参阅此处以获得更好的解释。 此外,ManualResetEvent 对 _a、_b 和 _c 的线程安全性没有任何影响。 您必须单独管理它。
编辑:通过这次编辑,我试图提取有关此问题的各种答案和评论中包含的所有信息。
基本问题是当标志变量 (_completed) 返回 true 时结果变量(_a、_b 和 _c)是否“可见”。
暂时,我们假设没有任何变量被标记为易失性的。 在这种情况下,可以在 Task() 中设置标志变量之后设置结果变量,如下所示:
这显然不是我们想要的,那么我们该如何处理呢?
如果这些变量被标记为易失性的,那么这种重新排序将被阻止。 但这就是引发最初问题的原因 - 是否需要 挥发性物质,或者 ManualResetEvent 是否提供隐式内存屏障,以便不会发生重新排序,在这种情况下,挥发性关键字并不是真正必要的?
如果我理解正确的是,wekempf 的立场是 WaitOne() 函数提供了一个隐式内存屏障来解决问题。 但是对我来说这似乎还不够。 主线程和后台线程可以在两个单独的处理器上执行。 因此,如果 Set() 不提供隐式内存屏障,那么 Task() 函数最终可能会在其中一个处理器上像这样执行(即使使用易失性变量)
:内存屏障和EventWaitHandles,我什么也没想出来。 我见过的唯一参考资料是 wekempf 对 Jeffrey Richter 的书所做的参考资料。 我遇到的问题是 EventWaitHandle 旨在同步线程,而不是访问数据。 我从未见过任何使用EventWaitHandle(例如ManualResetEvent)来同步数据访问的示例。 因此,我很难相信 EventWaitHandle 对内存屏障做了任何事情。 否则,我希望在互联网上找到一些对此的参考。
编辑 #2:这是对 wekempf 对我的回复的回复...;)
我设法在 amazon.com 上阅读了 Jeffrey Richter 书中的部分。 从第 628 页开始(wekempf 也引用了这一点):
因此,正如 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:
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):
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):
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.
等待函数具有隐式内存屏障。 请参阅 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
首先,我不确定我是否应该“回答我自己的问题”或为此使用注释,但这里是:
我的理解是,易失性阻止代码/内存优化将访问移动到我的结果变量(以及完成的布尔值) )这样读取结果的线程将看到最新的数据。
由于编译器或 emmpry 优化/重新排序,您不希望 _completed 布尔值对 Set() 之后的所有线程都可见。 同样,您也不希望在 Set() 之后看到对结果 _a、_b、_c 的写入。
编辑:关于马特·戴维斯提到的项目,对问题的进一步解释/澄清:
因此,你们都同意这样的操作负责处理器之间或寄存器等中的缓存。
但是它是否可以防止重新排序以保证结果都在完成标志之前分配,并且在设置 ManualResetEvent 之前,已完成标志被指定为 true 吗?
该示例的概念是执行一个可能长时间运行的任务。 如果任务可以在异常时间内完成,则调用线程将访问结果并继续正常处理。 但有时任务可能需要相当长的时间,并且请求线程在这段时间内不能被阻塞,并且可以采取合理的步骤来处理该问题。 这可能包括稍后使用 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:
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?
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.
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.
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.
根据您所展示的内容,我想说,不,该代码中不需要
易失性
。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. Allvolatile
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.