2

volatile关键字用于保护字段免受某些编译器优化:

对于非易失性字段,重新排序指令的优化技术可能会导致多线程程序在没有同步的情况下访问字段的意外和不可预测的结果,例如锁定语句提供的那些(第 8.12 节)

但是,MSDN 似乎没有明确说明是lock仅对.expressionstatement_block

如果我有一段代码:

lock(obj_something){
    boolFoo = true;
    if(boolIsFriday)
        doSomething(anything);
}

和隐含boolFooboolIsFriday易失性(即使它没有被声明volatile)?

4

5 回答 5

3

这是一个困难的话题,我建议你从阅读 Joe Duffy 的书开始Concurrent Programming on Windows——这是 1000 页纯知识。

回答您的问题:不,变量不会“隐式易变”

lock除了相互排斥之外,它确实提供的是获取和释放的栅栏。

这意味着对编译器、抖动、cpu 或介于两者之间的任何东西可以应用的重新排序优化设置了一些限制。

尤其:

获取锁可以防止任何内存访问在栅栏 之前移动。
释放锁可以防止任何内存访问在栅栏之后移动。


boolFoo在您的示例中,在获取锁之前,任何线程都无法观察到写入。

类似地,读取boolIsFridayanything不能使用在获取锁之前读取的值。

在释放时,在释放锁时写入boolFoo必须可见,该doSomething方法执行的任何写入也必须可见。


回答您的评论:这不会阻止其他线程重新排序。

如果您有另一个线程B执行此操作:

// do some work while your lock code is running
for ( int i = 0 ; i < ( int ) 1e7 ; i++ ) ;

var copyOfBoolFoo = boolFoo;

那么它可能copyOfBoolFoofalse

那是因为在循环和你的锁代码运行之前,某些东西可能会向前看for,并决定读取boolFoo和缓存该值。

线程中没有任何东西B可以防止这种情况发生,所以它可能会发生。

如果您在读取boolFoo​​in thread之前放置了一个栅栏(例如锁!) B,那么您保证读取最新值。

于 2013-04-05T17:53:00.020 回答
2

不,lock 语句仅 - 直接 - 保护对您“锁定”的对象的访问。

关键在于,如果您在多个地方使用一个对象作为锁源,那么由于拥有锁,因此在任何时候只有一个代码块(在锁范围内)可以运行。

boolFoo并且boolIsFriday仅当它们在同一对象的锁内被独占访问时才受到保护(在这种情况下obj_something

于 2013-04-05T17:43:24.337 回答
2

但是,MSDN 似乎没有明确说明 lock 是仅对表达式中使用的对象应用优化保护,还是对 statement_block 中的所有语句应用优化保护。

后者。

boolFoo 和 boolIsFriday 是否是隐含的 volatile(即使它没有被声明为 volatile)?

不完全,不。的想法volatile是在任何时候访问对象时都会应用内存屏障。lock仅将内存屏障应用于对象的那一种用途。因此,不会有从lock语句内部和外部混合(某些类型的已定义)操作的优化,但是如果其他东西在没有lock块的情况下访问该变量,您仍然可能会遇到麻烦。

于 2013-04-05T17:46:16.320 回答
2

让我们仔细看看您的示例,以帮助回答您的问题。我将稍微调整您的代码,以便在读取boolIsFriday发生时更清楚。行为仍然相同。这就是我们所拥有的。

lock(obj_something)
{
    boolFoo = true;
    var register1 = boolIsFriday;
    if (register1)
    {
        var register2 = anything;
        doSomething(register2);
    }
}

接下来,我们将用箭头符号装饰这段代码,以帮助可视化内存屏障的位置。我将使用 ↑ 箭头和 ↓ 箭头分别表示释放栅栏获取栅栏。易失性写入具有释放语义,因此它们将在写入之前放置一个 ↑ 。易失性读取具有获取语义,因此它们将在读取之后放置一个↓ 。把箭头想象成把一切都推离它。没有其他读取或写入可以在箭头头部上方或下方浮动。如果您以我刚刚描述的术语来考虑它,那么这应该准确地反映内存屏障的作用。它也恰好与规范一致。

在我们开始之前,还有几件事要提及。首先,虽然没有其他内存访问被允许越过箭头的头部,但它们仍然被允许越过尾部。同样,这符合规范的措辞。其次,一旦生成了内存屏障,它们就会被锁定到位。生成它们的机制,如易失性读取或写入,仍然可以自由移动。换句话说,箭头不能移动,但相应的读或写可以。第三,alock产生一个完整的内存屏障。


锁内的内存访问是否隐式易失?

为了回答这个问题,我将在您的示例中添加箭头符号。

↑
lock(obj_something)
↓
{
    boolFoo = true;
    var register1 = boolIsFriday;
    if (register1)
    {
        var register2 = anything;
        doSomething(register2);
    }
↑
}
↓

注意箭头的位置。代替没有其他内存生成器,只有lock注入它们。现在有几个事实应该是显而易见的。

  1. 锁块之外的任何内存访问都不能在块移动。
  2. 锁块的任何内存访问都不能移出块
  3. 只要满足长点#2,锁块内的所有内存访问都可以自由移动。

这应该会立即回答您的一个问题。不,boolFoo并且boolIsFriday不要从lock. 他们仍然可以自由移动,尽管现在有一些限制。

那么如果boolFooboolIsFriday都被标记为volatile. 让我们来看看会发生什么。

↑
lock(obj_something)
↓
{
    ↑
    boolFoo = true;
    var register1 = boolIsFriday;
    ↓
    if (register1)
    {
        var register2 = anything;
        doSomething(register2);
    }
↑
}
↓

注意箭头已放置。写入前有一个↑箭头boolFoo,表示没有其他内存访问可以通过易失性写入向下浮动。同样,读取后有一个↓箭头boolIsFriday表示没有其他内存访问可以通过易失性读取浮动。现在有两个特殊的事实显而易见。

  1. 写入boolFoo和读取boolIsFriday可以交换。请注意没有箭头可以防止这种情况发生。
  2. 写入boolFoo可以一直浮动到lock块的末尾(当然假设这dosomething不会产生内存屏障。正是 ↑ 箭头结束了锁定块,防止它进一步下降。
  3. 的读取boolIsFriday不能向下浮动很远,因为在某些时候它必须与读取的交换位置anythinganything但是,如果发生这种情况,那么这在语义上与在表示 volatile 读取的 ↓ 上方浮动相同。我们已经说过阻止了移动。

锁是否限制表达式或块内容的优化?

最后回答你的另一个问题。该lock关键字确实对锁定表达式和锁定块本身的内容都有一些影响。让我们来看看这是如何发生的。我设计了一个带有有趣表达式的 lock 语句。

object foo;
lock (foo=bar)
{
  // contents of lock here
}

你看到我做了什么吗?我在表达式中放置了一个赋值。如果我们重新组织它以更好地了解这里发生的事情,它会是什么样子。

object foo;
foo = bar;
object expression = foo;
↑
lock (expression)
↓
{
  // contents of lock here
↑
}
↓

现在注意以下几点。

  1. 由于语句生成的↑ ,write tofoo是不允许下浮的。lock这是锁表达式影响的一种方式。
  2. 我们已经讨论过锁块的内容现在对它们的移动施加了约束。

因此,要回答您的问题,lock关键字对锁定表达式和锁定块的内容都设置了优化约束。

于 2013-08-09T14:57:20.500 回答
0

保证块内的所有语句作为一个单元是有序的,相对于在一个持有相同锁的块内执行的任何其他语句,作为一个单元。不多也不少。

因此,如果您有:

lock (foo) // in one thread
{
  x();
  y();
}

lock (foo) // in another
{
  a();
  b();
}

您可以保证执行顺序是x y a ba b x y

您防止x a b y, x a y b, orx和同时a运行并爆炸。

于 2013-04-05T17:48:06.613 回答