本网站的新用户,如果我没有以可接受的方式发帖,请告诉我。
我经常在下面的示例中对某些内容进行编码(为了清楚起见,请使用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();
}
}
答案 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)
答案 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
不以任何方式同步对变量的访问。