以下是Joe Duffy的书(Windows上的Concurrent Programming)的片段,后面是该段所涉及的代码段。这段代码意味着在并发环境(由许多线程使用)中工作,其中此LazyInit<T>
类用于创建仅在真正需要在内部使用值(类型为T)时初始化的am对象代码。
如果有人可以详细说明无序负载加载可能会产生问题的逐步方案,我将不胜感激。也就是说,如果每个线程的加载顺序首先加载字段然后加载引用而不是我们期望的方式,那么使用该类并将引用及其字段分配给变量的两个或多个线程怎么可能是个问题呢?它是(先加载参考,然后加载通过参考获得的字段值?)
据我所知,这种情况很少发生(由于无序加载而失败)。事实上,我可以看到一个线程可以在不知道引用值(指针?)是什么的情况下首先错误地读取字段的值,但如果发生这种情况,那么该线程将自行纠正(就好像它不在并发环境)如果发现过早负载值不正确;在这种情况下,装载最终会成功。换句话说,另一个线程的存在怎么可能使加载线程不“意识到”加载线程中的无序加载是无效的?
我希望我设法传达问题,因为我真的看到了。
段:
由于上面提到的所有处理器,除了.NET内存模型之外,在某些情况下允许加载到加载重新排序,m_value的加载可能在加载对象的字段后移动。效果类似,将m_value标记为volatile会阻止它。将对象的字段标记为易失性是不必要的,因为读取值是获取栅栏并防止后续加载之前移动,无论它们是否是易失性的。这对某些人来说可能看起来很荒谬:在引用对象本身之前如何读取字段?这似乎违反了数据依赖性,但它没有:一些较新的处理器(如IA64)采用价值推测并将提前执行负载。如果处理器恰好猜测引用和字段的正确值,就像在写入引用之前那样,推测性读取可能会退出并产生问题。这种重新排序非常罕见,在实践中可能永远不会发生,但它仍然是一个问题。
代码示例:
public class LazyInitOnlyOnceRef<T> where T : class
{
private volatile T m_value;
private object m_sync = new object();
private Func<T> m_factory;
public LazyInitOnlyOnceRef(Func<T> factory) { m_factory = factory; }
public T Value
{
get
{
if (m_value == null)
{
lock (m_sync)
{
if (m_value == null)
m_value = m_factory();
}
}
return m_value;
}
}
}
答案 0 :(得分:4)
一些较新的处理器(如IA64)采用价值推测并将提前执行负载。如果处理器恰好猜测引用和字段的正确值,就像在写入引用之前那样,推测性读取可能会退出并产生问题。
这基本上对应于以下源转换:
var obj = this.m_value;
Console.WriteLine(obj.SomeField);
变为
[ThreadStatic]
static object lastValueSeen = null; //processor-cache
//...
int someFieldValuePrefetched = lastValueSeen.SomeField; //prefetch speculatively
if (this.m_value == lastValueSeen) {
//speculation succeeded (accelerated case). The speculated read is used
Console.WriteLine(someFieldValuePrefetched);
}
else {
//speculation failed (slow case). The speculated read is discarded.
var obj = this.m_value;
lastValueSeen = obj; //remember last value
Console.WriteLine(obj.SomeField);
}
处理器尝试预测加热缓存所需的下一个内存地址。
基本上,您不能再依赖数据依赖性,因为可以在指向包含对象的指针之前加载字段。
你问:
if(this.m_value == lastValueSeen)实际上是哪个语句 prdeiction(基于值,参见上一次每m_value) 考试。我理解在顺序编程中(非 并发),测试必须始终失败,无论最后的价值 看到了,但在并发编程中,测试(预测)可以 成功,处理器的执行流程将随之而来 打印无效值(i..e,null someFieldValuePrefetched)
我的问题是这种错误预测怎么可能 仅在并发编程中成功但在顺序编程中不成功 非并发编程。与此问题有关,在 当这个错误的预测被接受时,并发编程 处理器,m_value的可能值是什么(即,它必须是 null,非null)?
推测是否成功不依赖于线程,而是取决于this.m_value
是否与上次执行时的值相同。如果它很少变化,那么猜测通常会成功。
答案 1 :(得分:0)
首先,我必须说我非常感谢你在这件事上的帮助。 为了磨练我的理解,我就是这样看的,如果我错了,请纠正我。
如果线程T1要执行错误的推测加载路径,则将执行以下代码行:
Thread T1 line 1: int someFieldValuePrefetched = lastValueSeen.SomeField; //prefetch speculatively
Thread T1 line 2: if (this.m_value == lastValueSeen) {
//speculation succeeded (accelerated case). The speculated read is used
Thread T1 line 3: Console.WriteLine(someFieldValuePrefetched);
}
else {
//speculation failed (slow case). The speculated read is discarded.
…..
….
}
另一方面,线程T2需要执行以下代码行。
Thread T2 line 1: old = m_value;
Thread T2 line 2: m_value = new object();
Thread T2 line 3: old.SomeField = 1;
我的第一个问题是:当执行“Thread T1 line 1”时,this.m_value的含义是什么?我想它在执行“线程T2第2行”之前等于旧的m_value,对吗?否则,推测分支将不会选择加速路径 这让我想知道线程T2是否也必须按顺序执行其代码行。也就是说,它执行“线程T2线1”,“线程T2线3”,“线程T2线2”而不是“线程T2线1”,“线程T2线2”,“线程T2线3”?如果是这样,那么我相信volatile关键字也会阻止线程T2以乱序的方式执行代码,对吗?
我可以看到线程T1的“线程T1线2”在线程T2的“线程T2线1”和“线程T2线3”之后和“线程T2线2”之前执行,然后线程T1中的SomeField将是1,即使这没有意义,正如你所指出的那样,因为当SomeField变为1时,为m_value分配一个新值,对于SomeField,该值的值为0
答案 2 :(得分:0)
如果它仍然是实际的,请考虑以下代码,它来自Joe Duffy的CPOW:
MyObject mo = new LazyInit<MyObject>(someFactory).Value;
int f = mo.field;
if (f == 0)
{
//Do Something...
Console.WriteLine(f);
}
以下文字也来自书中“如果从初始读取mo.field到变量f之间的时间段以及随后在Console.WriteLine中使用f的时间足够长,编译器可能会决定它将是更有效地重读mo.field两次....如果保持该值会产生寄存器压力,编译器可能会决定这一点,导致堆栈空间使用效率降低:
...
if (mo.field == 0)
{
////Do Something...
Console.WriteLine(mo.field);
}
所以,我认为这可能是退役裁判的一个很好的例子。 到mo.field 的后续使用时,mo的推测性读取可能会退出并创建一个空引用异常,这肯定是个问题。