半围栏和全围栏?

时间:2012-05-14 19:14:36

标签: c# .net multithreading

我一直在阅读Full fences阻止任何类型的指令重新排序或缓存围栏(通过memoryBarrier)

然后我读到了生成“半围栏”的volatile

  

volatile关键字指示编译器生成   在每次从该字段读取时获取栅栏,并释放栅栏   每次写入该字段。

获得围栏

  

获取栅栏阻止其他读/写之前被移动   围栏;

释放围栏

  

释放栅栏可防止在移动后移动其他读/写   围栏。

有人可以用简单的英语向我解释这两句话吗?

(围栏在哪里?)

修改

在这里得到一些答案之后 - 我做了一个可以帮助每个人的图画 - 我想。

http://i.stack.imgur.com/A5F7P.jpg enter image description here

4 个答案:

答案 0 :(得分:13)

您所指的措辞看起来像我经常使用的措辞。规范说明了这一点:

  • 读取volatile字段称为volatile读取。易失性读取具有“获取语义”;也就是说,它保证在指令序列之后发生的任何内存引用之前发生。
  • 写入易失性字段称为易失性写入。易失性写入具有“释放语义”;也就是说,保证在指令序列中的写指令之前的任何存储器引用之后发生。

但是,我通常会使用您在问题中引用的措辞,因为我希望将重点放在指令可以移动这一事实上。您引用的措辞和规范是等效的。

我将介绍几个例子。在这些例子中,我将使用一个特殊的符号,它使用↑箭头表示释放栅栏,↓箭头表示获取栅栏。没有其他指令可以通过↑箭头向上浮动或超过↓箭头。把箭头想象成排斥它的一切。

请考虑以下代码。

static int x = 0;
static int y = 0;

static void Main()
{
  x++
  y++;
}

重写它以显示各个指令看起来像这样。

static void Main()
{
  read x into register1
  increment register1
  write register1 into x
  read y into register1
  increment register1
  write register1 into y
}

现在,因为在这个例子中没有内存障碍,C#编译器,JIT编译器或硬件可以通过许多不同的方式自由地优化它,只要执行线程所感知的逻辑序列与物理序列。这是一个这样的优化。请注意如何交换xy的读写操作。

static void Main()
{
  read y into register1
  read x into register2
  increment register1
  increment register2
  write register1 into y
  write register2 into x
}

现在这次将这些变量更改为volatile。我将使用箭头符号来标记记忆障碍。请注意如何保留对xy的读写顺序。这是因为指令不能越过我们的障碍(用↓和↑箭头表示)。现在,这很重要。请注意,x指令的增量和写入仍然允许向下浮动并且y的读取浮动。这仍然有效,因为我们使用半围栏。

static volatile int x = 0;
static volatile int y = 0;

static void Main()
{
  read x into register1
  ↓    // volatile read
  read y into register2
  ↓    // volatile read
  increment register1
  increment register2
  ↑    // volatile write
  write register1 into x
  ↑    // volatile write
  write register2 into y
}

这是一个非常简单的例子。请查看我的回答here,了解volatile如何在双重检查模式中产生差异的非平凡示例。我使用与此处使用的相同的箭头符号,以便于查看正在发生的事情。

现在,我们还可以使用Thread.MemoryBarrier方法。它会产生一个完整的围栏。因此,如果我们使用箭头符号,我们可以想象它是如何工作的。

考虑这个例子。

static int x = 0;
static int y = 0;

static void Main
{
  x++;
  Thread.MemoryBarrier();
  y++;
}

如果我们要像以前一样显示个别说明,那么这看起来像这样。请注意,现在完全阻止了指令移动。在不影响指令的逻辑顺序的情况下,实际上没有其他方法可以执行。

static void Main()
{
  read x into register1
  increment register1
  write register1 into x
  ↑    // Thread.MemoryBarrier
  ↓    // Thread.MemoryBarrier
  read y into register1
  increment register1
  write register1 into y
}

好的,还有一个例子。这次让我们使用VB.NET。 VB.NET没有volatile关键字。那么我们如何模仿VB.NET中的易失性读取呢?我们将使用Thread.MemoryBarrier 1

Public Function VolatileRead(ByRef address as Integer) as Integer
  Dim local = address
  Thread.MemoryBarrier()
  Return local
End Function

这就是我们用箭头符号看起来的样子。

Public Function VolatileRead(ByRef address as Integer) as Integer
  read address into register1
  ↑    // Thread.MemoryBarrier
  ↓    // Thread.MemoryBarrier
  return register1
End Function

重要的是要注意,由于我们想模仿易失性读取,因此必须在实际读取之后将Thread.MemoryBarrier的调用置于。不要陷入认为易失性读取意味着“新读”并且易失性写意味着“提交写入”的陷阱。这不是它的工作原理,它肯定不是规范所描述的。

<强>更新

参考图像。

  

等待!我证实所有的写作都已完成!

  

等待!我正在验证所有消费者都已获得最新消息   值!

这是我正在谈论的陷阱。这些陈述并不完全准确。是的,在硬件级别实现的内存屏障可以同步缓存一致性线,因此上述语句可能在某种程度上准确地说明了所发生的事情。但是,volatile只会限制指令的移动。该规范说 nothing 关于从内存加载值或将内存存储到内存屏障的位置。


1 当然,Thread.VolatileRead内置了。你会注意到它的实现与我在这里完全一样。

答案 1 :(得分:1)

从另一个方面开始:

阅读volatile字段时有什么重要意义?之前所有写入该字段的内容都已提交。

写入易失性字段时,重要的是什么?所有以前的读取都已经得到了它们的值。

然后尝试验证在这些情况下获取栅栏和释放栅栏是否有意义。

答案 2 :(得分:1)

为了更容易理解这一点,让我们假设一个可以重新排序的内存模型。

让我们看一个简单的例子。假设这个易变的字段:

volatile int i = 0;

这个读写序列:

1. int a = i;
2. i = 3;

对于指令1,即i的读取,生成获取栅栏。这意味着对i的写入的指令2不能与指令1重新排序,因此a在序列结束时不可能为3。

现在,当然,如果您考虑单个线程,但如果另一个线程对相同的值进行操作(假设a是全局的),则上述内容没有多大意义:

thread 1               thread 2
a = i;                 b = a;
i = 3;

在这种情况下,您认为线程2不可能将值3获取到b(因为它会在赋值之前或之后获得a的值{{ 1}})。但是,如果对a = i;的读取和写入进行了重新排序,则i可能会获得值3.在这种情况下,如果您的程序正确性取决于{{{{}}},则必须使b为volatile。 1}}不会变成3。

免责声明:以上示例仅供理论之用。除非编译器完全疯了,否则不会去做可能为变量创建“错误”值的重新排序(即使i不是易变的,b也不能为3)。

答案 3 :(得分:1)

来自volatile (C# Reference)

  

volatile关键字表示某个字段可能被同时执行的多个线程修改。

为了使程序运行得更快,.NET有时(通常在优化时)会执行智能操作,例如,如果在下一个命令中将更改变量,则不会将变量写入内存:

int i = 0;
//Do some stuff.
i++;
//Do some more stuff.
i--;
//Do other stuff.

这里,编译器会将i的值存储在寄存器中,直到i--;完成。这节省了从RAM中获取值的少量时间。

如果在线程之间共享i,则线程化时不起作用。例如,你可能有:

//Thread 1:
i = 0;      //i is a volatile int shared between threads.
//Do some stuff.
//Wait for Thread 2 to read i.
i++;
//Do some more stuff.
//Wait for Thread 2 to set i = 12.
i--;
//Do other stuff.
//Use i for something like an index.

如果线程1和2将i存储在寄存器中,则更改线程1中的i将不会影响线程2中的i。易失性告诉编译器可以从多个线程访问此变量(i)。因此,它应始终从内存中获取当前值,并将任何更新的值记录到内存中。

另一个例子是SQL表中的值,任何人都可以随时更改该值。正常变量就像查询表一次,然后在本地使用该值,直到完成它为止。挥发性变量就像查询表一样,每次需要时获取/设置最新值,这样每个人都可以访问当前值。

查看volatile (C# Reference)中的示例,因为它提供了如何使用volatile变量的一个很好的示例。

如果您想要更多,请告诉我们。