细说.NET中的多线程 (六 使用MemoryBarrier,Volatile进行同步)

上一节介绍了使用信号量进行同步,本节主要介绍一些非阻塞同步的方法。本节主要介绍MemoryBarrier,volatile,Interlocked。c#

MemoryBarriers

本文简单的介绍一下这两个概念,假设下面的代码:缓存

using System;
class Foo
{
    int _answer;
    bool _complete;

    void A()
    {
        _answer = 123;
        _complete = true;
    }

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

若是方法A和方法B同时在两个不一样线程中运行,控制台可能输出0吗?答案是可能的,有如下两个缘由:安全

  • 编译器,CLR或者CPU可能会更改指令的顺序来提升性能
  • 编译器,CLR或者CPU可能会经过缓存来优化变量,这种状况下对其余线程是不可见的。

最简单的方式就是经过MemoryBarrier来保护变量,来防止任何形式的更改指令顺序或者缓存。调用Thread.MemoryBarrier会生成一个内存栅栏,咱们能够经过如下的方式解决上面的问题:多线程

using System;
using System.Threading;
class Foo
{
    int _answer;
    bool _complete;

    void A()
    {
        _answer = 123;
        Thread.MemoryBarrier();    // Barrier 1
        _complete = true;
        Thread.MemoryBarrier();    // Barrier 2
    }

    void B()
    {
        Thread.MemoryBarrier();    // Barrier 3
        if (_complete)
        {
            Thread.MemoryBarrier();       // Barrier 4
            Console.WriteLine(_answer);
        }
    }
}

上面的例子中,barrier1和barrier3用来保证指令顺序不会改变,barrier2和barrier4用来保证值变化不被缓存。一个好的处理方案就是咱们在须要保护的变量先后分别加上MemoryBarrier。性能

在c#中,下面的操做都会生成MemoryBarrier:优化

  • Lock语句(Monitor.Enter,Monitor.Exit)
  • 全部Interlocked类的方法
  • 线程池的回调方法
  • Set或者Wait信号
  • 全部依赖于信号灯实现的方法,如starting或waiting 一个Task

由于上面这些行为,这段代码其实是线程安全的:线程

        int x = 0;
        Task t = Task.Factory.StartNew(() => x++);
        t.Wait();
        Console.WriteLine(x);    // 1

在你本身的程序中,你可能重现不出来上面例子所说的状况。事实上,从msdn上对MomoryBarrier的解释来看,只有对顺序保护比较弱的多核系统才须要用到MomoryBarrier。可是有一点须要注意:多线程去修改变量而且不使用任何形似的锁或者内存栅栏是会带来必定的麻烦的。blog

下面一个例子可以很好的说明上面的观点(在你的VisualStudio中,选择Release模式,而且Start Without Debugging重现这个问题):内存

        bool complete = false;
        var t = new Thread(() =>
        {
            bool toggle = false;
            while (!complete) toggle = !toggle;
        });
        t.Start();
        Thread.Sleep(1000);
        complete = true;
        t.Join();        // Blocks indefinitely

这个程序永远不会结束,由于complete变量被缓存在了CPU寄存器中。在while循环中加入Thread.MemoryBarrier能够解决这个问题。开发

volatile关键字

另一种更高级的方式来解决上面的问题,那就是考虑使用volatile关键字。Volatile关键字告诉编译器在每一次读操做时生成一个fence,来实现保护保护变量的目的。具体说明能够参见msdn的介绍

VolatileRead和VolatileWrite

Volatile关键字只能加到类变量中。本地变量不能被声明成volatile。这种状况你能够考虑使用System.Threading.Volatile.Read方法。咱们看一下System.Threading.Volatile源码如何实现这两个方法的:

    public static bool Read(ref bool location)
    {
        bool flag = location;
        Thread.MemoryBarrier();
        return flag;
    }
    public static void Write(ref bool location, bool value)
    {
        Thread.MemoryBarrier();
        location = value;
    }

  

一目了然,经过MemoryBarrier来实现的,可是他只在读操做的后面和写操做的前面加了MemoryBarrier,那么你应该考虑,若是你先使用Volatile.Write再使用Volatile.Read是否是可能有问题呢?

c#中ConcurrentDictionary中使用了Volatile类来保护变量,有兴趣的读者能够看看c#的开发者是如何使用这个方法来保护变量的。

Interlocked

使用MemoryBarrier并不老是一个好的解决方案,尤为在不须要锁的状况下。Interlocked方法提供了一些经常使用的原子操做来避免前面文章提到的一系列的问题。如使用Interlocked.Increment来替代++,Interlocked.Decrement来替代--。Msdn的文档中详细的介绍了相关的用法和原理。C#中的源码里也常常能看见Interlocked相关的使用。

 

本文介绍了一些除了锁和信号量以外的一些同步方式,欢迎批评与指正。

相关文章
相关标签/搜索