编程语言
首页 > 编程语言> > C#多线程之高级篇(上)

C#多线程之高级篇(上)

作者:互联网

一、非阻塞同步

重排序与缓存

我们观察下面这个例子:

public class Foo
{
    private int _answer;
    private bool _complete;

    void A() //A 1
    {
        _answer = 10;
        _complete = true;
    }

    void B() //B 2
    {
        if (_complete) Console.WriteLine(_answer);
    }
}

如果方法AB在不同的线程上并发运行,B可能会打印 “ 0 “ 吗?答案是会的,原因如下:

请务必重视它们,它们将是幽灵般的存在

int x = 0, y = 0, a = 0, b = 0;

var task1 = Task.Run(() => // A 1
{
    a = 1; // 1
    x = b; // 2
});
var task2 = Task.Run(() => // B 2
{
    b = 2; // 3
    y = a; // 4
});
Task.WaitAll(task1, task2);
Console.WriteLine("x:" + x + " y:" + y);

直觉和经验告诉我们,程序至顶向下执行:代码1一定发生在代码2之前,代码3一定发生在代码4之前,然鹅

在一个独立的线程中,每一个语句的执行顺序是可以被保证的,但在不使用lock,waithandle这样的显式同步操作时,我们就没法保证事件在不同的线程中看到的执行顺序是一致的了。尽管线程A中一定需要观察到a=1执行成功之后才会去执行x=b,但它没法确保自己观察得到线程B中对b的写入,所以A还可能会打印出y的一个旧版的值。这就叫指令重排序。

x:0 y:1 #1-2-3-4
x:2 y:0 #3-4-1-2
x:2 y:1 #1-3-2-4

可实际运行时还是有些让我们惊讶的情况:

x:0 y:0 #??

这就是缓存问题,如果两个线程在不同的CPU上执行,每一个核心有自己的缓存,这样一个线程的写入对于其它线程,在主存同步之前就是不可见的了。

C#编译器和CLR运行时会非常小心的保证上述优化不会破坏普通的单线程代码,和正确使用锁的多线程代码。但有时,你仍然需要通过显示的创建内存屏障(memory barrier,也称作内存栅栏 (memory fence))来对抗这些优化,限制指令重排序和读写缓存产生的影响。

内存屏障

参考博客小林野夫

处理器支持哪种内存重排序(LoadLoad重排序、LoadStore重排序、StoreStore重排序、StoreLoad重排序),就会提供相对应能够禁止重排序的指令,而这些指令就被称之为内存屏障(LoadLoad屏障、LoadStore屏障、StoreStore屏障、StoreLoad屏障)

屏障名称 示例 具体作用
StoreLoad Store1;Store2;Store3;StoreLoad;Load1;Load2;Load3 禁止StoreLoad重排序,确保屏障之前任何一个写(如Store2)的结果都会在屏障后任意一个读操作(如Load1)加载之前被写入
StoreStore Store1;Store2;Store3;StoreStore;Store4;Store5;Store6 禁止StoreStore重排序,确保屏障之前任何一个写(如Store1)的结果都会在屏障后任意一个写操作(如Store4)之前被写入
LoadLoad Load1;Load2;Load3;LoadLoad;Load4;Load5;Load6 禁止LoadLoad重排序,确保屏障之前任何一个读(如Load1)的数据都会在屏障后任意一个读操作(如Load4)之前被加载
LoadStore Load1;Load2;Load3;LoadStore;Store1;Store2;Store3 禁止LoadStore重排序,确保屏障之前任何一个读(如Load1)的数据都会在屏障后任意一个写操作(如Store1)的结果被写入高速缓存(或主内存)前被加载

读屏障告诉处理器在执行任何的加载前,执行所有已经在失效队列(Invalidte Queues)中的失效(I)指令。即:所有load barrier之前的store指令对之后(本核心和其他核心)的指令都是可见的。

Store Memory Barrier:写屏障,等同于前文的StoreStore Barriers 将store buffer都写入缓存。

写屏障告诉处理器在执行这之后的指令之前,执行所有已经在存储缓存(store buffer)中的修改(M)指令。即:所有store barrier之前的修改(M)指令都是对之后的指令可见。

最简单的内存屏障是完全内存屏障(full memory barrier,或全栅栏(full fence)),它可以阻止所有跨越栅栏的指令进行重排并提交修改和刷新缓存。内存屏障之前的所有写操作都要写入内存,并将内存中的新值刷到缓存,使得其它CPU核心能够读取到最新值,完全保证了数据的强一致性,进而解决CPU缓存带来的可见性问题。

标签:C,C++,java,C#,php,编程,语言,新人,人群,计算机,入门,Python
来源: