假设您有特性public Foo Bar { get; }
您想要延迟初始化。一种这样的方法可能是使用 Interlocked
类,它保证某些操作序列的原子性(例如递增、添加、比较交换)。你可以这样做:
private Foo _bar;
public Foo Bar
{
get
{
// Initial check, necessary so we don't call new Foo() every time when invoking the method
if (_bar == null)
{
// In the unlikely case 2 threads come in here
// at the same time, new Foo() will simply be called
// twice, but only one of them will be set to _bar
Interlocked.CompareExchange(ref _bar, new Foo(), null);
}
return _bar;
}
}
有很多地方演示了这种延迟初始化的方法,例如this blog和 places in the .NET Framework itself .
我的问题是,不应该读取 _bar
不稳定?例如,线程 1 可能调用 CompareExchange
,设置_bar
的值,但该更改对线程 2 不可见,因为(如果我正确理解 this question )它可能已缓存 _bar
的值为 null,它最终可能会调用 Interlocked.CompareExchange
再次,尽管线程 1 已经设置了 _bar
。所以不应该_bar
被标记为 volatile ,以防止这种情况发生?
简而言之,就是this approach或this approach延迟初始化正确吗?为什么是Volatile.Read
(与将变量标记为 volatile 并从中读取具有相同的效果)在一种情况下使用,但在另一种情况下不使用?
编辑TL;DR:如果一个线程通过 Interlocked.CompareExchange
更新字段的值,执行该字段的非 volatile 读取的其他线程会立即看到更新后的值吗?
最佳答案
我的第一个想法是“谁在乎呢?” :)
我的意思是:双重检查初始化模式几乎总是矫枉过正,而且很容易出错。大多数时候,一个简单的lock
最好:易于编写,性能足够,并且清楚地表达了代码的意图。此外,我们现在有Lazy<T>
类来抽象延迟初始化,进一步消除了我们手动实现代码来执行此操作的任何需要。
因此,双重检查模式的细节并不是那么重要,因为我们无论如何都不应该使用它。
也就是说,我同意您的观点,即读取应该是 volatile 读取。如果没有它, Interlocked.CompareExchange()
提供的内存屏障不一定有帮助。
不过有两件事可以缓解这个问题:
- 无论如何都不能保证双重检查模式。即使您有 volatile 读取,也存在竞争条件,因此必须安全地初始化两次。因此,即使内存位置已过时,也不会发生真正糟糕的情况。您将调用
Foo
构造函数两次,这不太好,但它不会是一个致命的问题,因为无论如何都有可能发生。 - 在 x86 上,默认情况下内存访问是 volatile 的。因此,实际上只有在其他平台上这才会成为问题。
关于c# - 对 Interlocked.CompareExchange 延迟初始化的字段执行定期读取是否正确?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/38298212/