dle*_*lev 12 .net multithreading
我最近在Resharper网站上看到了以下帖子.这是对双重检查锁定的讨论,并具有以下代码:
public class Foo
{
private static volatile Foo instance;
private static readonly object padlock = new object();
public static Foo GetValue()
{
if (instance == null)
{
lock (padlock)
{
if (instance == null)
{
instance = new Foo();
instance.Init();
}
}
}
return instance;
}
private void Init()
{
...
}
}
Run Code Online (Sandbox Code Playgroud)
该帖子然后声称
如果我们假设Init()是用于初始化Foo状态的方法,那么由于内存模型不能保证读写顺序,上述代码可能无法按预期运行.因此,对Init()的调用实际上可能在变量实例处于一致状态之前发生.
这是我的问题:
我的理解是,.NET内存模型(至少从2.0开始)并没有要求instance被声明为volatile,因为lock它将提供一个完整的内存栅栏.是不是这样,还是我被误导了?
对于多个线程,是不是只对可观察的读/写重新排序?我的理解是,在一个线程上,副作用将是一致的顺序,并且lock就地防止任何其他线程观察到某些东西是不对的.我也在这里吗?
Tim*_*oyd 24
该示例的一个大问题是第一个空检查未锁定,因此实例可能不是null,而是在调用Init 之前.这可能导致在调用Init之前使用实例的线程.
因此,正确的版本应该是:
public static Foo GetValue()
{
if (instance == null)
{
lock (padlock)
{
if (instance == null)
{
var foo = new Foo();
foo.Init();
instance = foo;
}
}
}
return instance;
}
Run Code Online (Sandbox Code Playgroud)
Bri*_*eon 16
据我所知,.NET内存模型(至少从2.0开始)并没有要求将该实例声明为volatile,因为lock会提供一个完整的内存区域.是不是这样,还是我被误导了?
这是必需的.原因是因为你在instance外面访问lock.让我们假设你省略volatile并且已经修复了这样的初始化问题.
public class Foo
{
private static Foo instance;
private static readonly object padlock = new object();
public static Foo GetValue()
{
if (instance == null)
{
lock (padlock)
{
if (instance == null)
{
var temp = new Foo();
temp.Init();
instance = temp;
}
}
}
return instance;
}
private void Init() { /* ... */ }
}
Run Code Online (Sandbox Code Playgroud)
在某种程度上,C#编译器,JIT编译器或硬件可以发出一个指令序列,该序列优化掉temp变量并使instance变量在Init运行之前被分配.实际上,它instance甚至可以在构造函数运行之前进行分配.该Init方法使问题更容易发现,但问题仍然存在于构造函数中.
这是一个有效的优化,因为指令可以在锁中自由重新排序.A lock会释放内存屏障,但仅限于Monitor.Enter和Monitor.Exit调用.
现在,如果省略volatile代码,则可能仍然可以使用大多数硬件和CLI实现组合.原因是因为x86硬件具有更紧密的内存模型,而且微软的CLR实现也非常紧张.但是,关于此主题的ECMA规范相对宽松,这意味着CLI的另一个实现可以自由地进行Microsoft当前选择忽略的优化.您必须为较弱的模型编写代码,这可能是CLI抖动,而不是大多数人倾向于关注的硬件.这就是为什么volatile仍然需要.
对于多个线程,是不是只对可观察的读/写重新排序?
是.只有当多个线程访问同一个内存位置时,指令重新排序才会起作用.即使是最弱的软件和硬件内存模型也不允许任何类型的优化,这些优化会改变开发人员在线程上执行代码时的行为.否则,没有程序会正确执行.问题在于其他线程如何观察该线程中发生的事情.其他线程可能会感觉到与执行线程的行为不同的行为.但是,执行线程始终会感知到正确的行为.
我的理解是,在单个线程上,副作用将是一致的顺序,并且锁定到位将阻止任何其他线程观察到某些东西是不对的.我也在这里吗?
不,一个lock人本身不会阻止其他线程感知不同的事件序列.原因是执行线程可能以lock不同于开发人员预期的顺序执行其中的指令.只有在锁的入口和出口点才会创建内存屏障.因此,在您的示例中,instance即使您已使用a包装这些指令,也可以在构造函数运行之前分配对新对象的引用lock.
使用volatile,而另一方面,对如何在代码里面有更大的影响lock相比,在初步核实,作为的行为instance在共同尽管智慧的方法的开头.很多人认为主要问题是如果instance没有易变的读数可能会陈旧.情况可能就是这样,但更大的问题是,如果没有lock另一个线程内部的易失性写入,可能会看到instance引用构造函数尚未运行的实例.volatile写入解决了这个问题,因为它阻止编译器在写入后移动构造函数代码instance.这volatile仍然是需要的重要原因.
| 归档时间: |
|
| 查看次数: |
2484 次 |
| 最近记录: |