以下是Joe Duffy的书(《Windows并发编程》)中的一小段摘录,后面跟着一个代码片段。代码片段用于并发环境(由许多线程使用)中,在这种情况下,
如果有人可以详细说明逐步场景如何导致无序的加载-到-加载(create a problem),我将不胜感激。也就是说,如果一个线程先加载字段再加载引用,而不是我们期望的先加载引用,然后通过引用得到字段的值,两个或多个使用该类并将引用及其字段赋值给变量的线程如何有问题?
我知道这种情况很少发生(由于加载的无序性而失败)。实际上,我能够看到一个线程可能会在不知道引用值(指针?)的情况下先错误地读取字段值, 但是,如果发生这种情况,则该线程会自行更正(就好像它不在并发环境中一样),如果注意到预读取值不正确,那么最终加载将成功。换句话说,另一个线程的存在如何使加载线程不“意识到”加载线程中的无序加载是无效的?
我希望我真正看到问题的传达方式是正确的。
摘录:
因为上述所有处理器(以及.NET内存模型)在某些情况下允许加载到加载重排,所以m_value的加载可能会在对象字段的加载之后移动。效果将类似,并且将m_value标记为易失性可以防止它。将对象的字段标记为易失性并不是必需的,因为值的读取是获取栅栏并防止后续加载移动之前的,无论它们是否易失性。这可能对一些人来说看起来很荒谬:如何在引用对象本身之前读取字段?这似乎违反数据依赖性,但实际上并非如此:一些新的处理器(如IA64)采用值猜测,将提前执行加载。如果处理器恰好猜测引用被写入之前的正确值和字段值,则可能出现推测读取可退休并创建问题。这种重新排序非常罕见,实际上可能永远不会发生,但仍然是一个问题。
代码示例:
LazyInit<T>
类用于创建仅在实际需要使用代码中的值(类型为T)时才初始化的对象。如果有人可以详细说明逐步场景如何导致无序的加载-到-加载(create a problem),我将不胜感激。也就是说,如果一个线程先加载字段再加载引用,而不是我们期望的先加载引用,然后通过引用得到字段的值,两个或多个使用该类并将引用及其字段赋值给变量的线程如何有问题?
我知道这种情况很少发生(由于加载的无序性而失败)。实际上,我能够看到一个线程可能会在不知道引用值(指针?)的情况下先错误地读取字段值, 但是,如果发生这种情况,则该线程会自行更正(就好像它不在并发环境中一样),如果注意到预读取值不正确,那么最终加载将成功。换句话说,另一个线程的存在如何使加载线程不“意识到”加载线程中的无序加载是无效的?
我希望我真正看到问题的传达方式是正确的。
摘录:
因为上述所有处理器(以及.NET内存模型)在某些情况下允许加载到加载重排,所以m_value的加载可能会在对象字段的加载之后移动。效果将类似,并且将m_value标记为易失性可以防止它。将对象的字段标记为易失性并不是必需的,因为值的读取是获取栅栏并防止后续加载移动之前的,无论它们是否易失性。这可能对一些人来说看起来很荒谬:如何在引用对象本身之前读取字段?这似乎违反数据依赖性,但实际上并非如此:一些新的处理器(如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;
}
}
}