并发环境中的乱序加载

WIT*_*ITL 7 c# concurrency multithreading

下面是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;
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

usr*_*usr 4

一些较新的处理器(如 IA64)采用值推测并提前执行加载。如果处理器碰巧猜测到引用和字段的正确值(如写入引用之前一样),则推测性读取可能会退出并产生问题。

这本质上对应于以下源转换:

var obj = this.m_value;
Console.WriteLine(obj.SomeField);
Run Code Online (Sandbox Code Playgroud)

变成

[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);
}
Run Code Online (Sandbox Code Playgroud)

处理器尝试预测预热缓存所需的下一个内存地址。

本质上,您不能再依赖数据依赖性,因为可以在知道指向包含对象的指针之前加载字段。


你问:

if (this.m_value == lastValueSeen) 实际上是测试预测(基于每个 m_value 上次看到的值)的语句。我知道在顺序编程(非并发)中,无论最后看到的值是什么,测试都必须始终失败,但在并发编程中,测试(预测)可能会成功,并且处理器的执行流程将随之发生,以尝试打印无效值(即,null someFieldValuePrefetched)

我的问题是,这种错误的预测怎么可能只在并发编程中成功,而在顺序、非并发编程中却不能成功。关于这个问题,在并发编程中,当处理器接受这个错误预测时,m_value 的可能值是多少(即,它必须为 null,非 null)?

推测是否有效并不取决于线程,而是取决于是否this.m_value通常与上次执行时的值相同。如果它很少变化,那么猜测往往会成功。