线程平安

多个线程试图同时拜访同一个数据时,数据不会受到毁坏

线程同步结构

结构模式别离有用户模式和内核模式两种,其中用户模式结构应用了非凡的CPU指令协调线程(协调是在硬件中产生的事件),所以其结构速度要显著快于内核模式结构,同时用户模式中阻塞的线程池线程永远不会被认为阻塞,所以线程池不会创立新线程替换阻塞线程。在用户模式中运行的线程可能被零碎抢占,但线程会以最快的速度再次调度,所以想要获取某一资源又临时无奈获得时,线程会用户模式中始终运行,这并不是一个良好的景象。而内核模式的结构是由Windows操作系统本身提供的,要求在应用程序的线程中调用在操作系统内核中实现的函数,将线程从用户模式切换为内核模式会造成微小的性能损失。然而也有一个长处:一个线程应用内核模式结构获取一个由其它线程正在拜访的资源时,Windows会阻塞线程,使之不再节约CPU工夫,等到资源可用时会复原线程,容许它拜访资源。

用户模式结构
  • 易失结构:在蕴含一个简略数据类型的变量上执行原子性的读或写操作
  • 互锁结构:在蕴含一个简略数据类型的变量上执行原子性的读和写操作
原子性

指事务的不可分割性,意味着一个变量的值的读取都是一次性的,如以下代码

class SomeType{    public static int x;}SomeType.x = 0x01234567;

变量x会一次性从0x00000000变成0x01234567,另一个线程不可能看到一个处于两头值的状态,如0x01234000,这便是原子性。

易失结构

编写好的代码须要被编译器编译成IL代码,再通过JIT编译器转换老本地CPU指令能力被计算机执行。而在这些转换过程中,编译器、JIT编译器、CPU自身可能都会对原先编写好的代码进行优化。如上面这段代码通过编译后将会隐没

private static void SomeMethod(){    //常量表达式在编译时计算为0    int value = 100 - (50 * 2);    //value为0循环永不执行    for (int i = 0; i < value; i++)    {        //永远执行不到,不须要编译循环中的代码        Console.WriteLine(i);    }}

上述代码中,编译器发现value为0,循环永远不会执行,没有必要编译循环中的代码,因而这个办法编译后会被优化掉。如果有一个办法中调用了SomeMethod办法,在对这个办法进行JIT编译的时候,JIT编译器会尝试内联SomeMethod办法的代码,因为没有代码,所以JIT编译器会删除调用SomeMethod办法的代码。

编译器、JIT编译器和CPU对代码进行优化的时候,从单线程的角度看,代码会做咱们心愿它做的事件,而从多线程来看,代码的用意不肯定会失去保留,以下的代码进行了演示:

class SomeType{    private int m_Flag = 0;    private int m_Value = 0;    public void Thread1()    {        this.m_Value = 10;        this.m_Flag = 1;    }    public void Thread2()    {        //可能会输入0,与预期不统一        if(this.m_Flag == 1)            Console.WriteLine("value = {0}", this.m_Value);    }}static void Main(){    ThreadPool.QueueUserWorkItem((o) =>    {        someType.Thread1();    });    ThreadPool.QueueUserWorkItem((o) =>    {        someType.Thread2();    });}

上述代码的问题在于假设Thread1办法中的代码依照程序执行,编译Thread2办法中的代码时,编译器必须生成代码将m_Flag和m_Value 从RAM读入CPU寄存器。RAM可能先传递m_Value的值(此时为0),而后Thread1可能执行,将Thread1改为10,m_Flag改为1。然而Thread2的CPU寄存器没有看到m_Value的值曾经被另一个线程批改为10,呈现输入后果为0的状况。除此之外Thread1办法中的两行代码在CUP/编译器在解释代码时可能会呈现反转,毕竟这样做也不会扭转代码的用意,同样可能呈现在Thread2中m_Value输入0的状况。

批改代码以修复问题,批改后的代码如下:
class SomeType{    private int m_Flag = 0;    private int m_Value = 0;    public void Thread1()    {        this.m_Value = 10;        Thread.VolatileWrite(ref this.m_Flag, 1);    }    public void Thread2()    {        if (Thread.VolatileRead(ref this.m_Flag) == 1)            Console.WriteLine("value = {0}", this.m_Value);    }}

批改后的代码能够看到别离应用了VolatileWrite和VolatileRead来读写数据,Thread1办法调用VolatileWrite能够确保后面的所有数据都写入实现才会将1写入m_Flag;Thread2办法调用VolatileRead能够确保必须先读取m_Flag的值能力读取m_Value的值。

VolatileWrite和VolatileRead
  • VolatileWrite:强制address中的值在调用时写入,除此之外还必须依照程序,即所有产生在VolatileWrite之前的加载和存储操作必须先于调用VolatileWrite办法实现
  • VolatileRead:强制address中的值在调用时读取,除此之外还必须依照程序,即所有产生在VolatileRead之后的加载和存储操作必须晚于调用VolatileRead办法实现
volatile关键字
class SomeType{    private volatile int m_Flag = 0;    private int m_Value = 0;    public void Thread1()    {        this.m_Value = 10;        this.m_Flag = 1;    }    public void Thread2()    {        if (this.m_Flag == 1)            Console.WriteLine("value = {0}", this.m_Value);    }}

应用volatile关键字能够达到和调用VolatileWrite和VolatileRead雷同的成果,除此之外volatile关键字通知C#和JIT编译器不将字段缓存到CPU寄存器中,确保字段的所有读写都在RAM中进行。

调用VolatileWrite办法或VolatileRead办法、应用volatile关键字将会禁用C#编译器、JIT编译器和CPU自身所执行的一些代码优化,如果使用不当反而会侵害性能。并且C#不反对以传援用的形式将volatile润饰的字段传递给办法。

自旋锁
struct SpinLock{    private int m_ResourceInUse;    public void Enter()    {        //将资源设置为正在应用,并返回m_ResourceInUse的原始值        while (Interlocked.Exchange(ref this.m_ResourceInUse, 1) != 0) { }    }    public void Leave()    {        //开释资源        Thread.VolatileWrite(ref this.m_ResourceInUse, 0);    }}private static SpinLock s_SpinLock = new SpinLock();private static void DoSomething(){    s_SpinLock.Enter();    //一次只有一个线程能力进入这里执行代码    s_SpinLock.Leave();}

当初如果两个线程同时调用Enter,Interlocked.Exchange会确保其中一个线程将m_ResourceInUse从0变到1,并返回m_ResourceInUse的原始值0,而后线程从Enter返回,继续执行前面的代码。另一个线程会将m_ResourceInUse从1变到1,并返回原始值1,发现不是将m_ResourceInUse从0变成1的,所以会始终调用Interlocked.Exchange开始自旋,直到第一个线程调用Leave。第一个线程调用Leave后,会将m_ResourceInUse从新变成0,这时正在自旋的线程调用Interlocked.Exchange可能将m_ResourceInUse从0变成1,于是从Enter返回继续执行后续的代码。

自旋锁的毛病在于处于自旋的线程无奈做其它的工作,节约CPU工夫,倡议只将自旋锁用于爱护执行得十分快的代码块。

内核结构结构
内核模式结构的毛病

因为须要Windows操作系统的本身合作以及内核对象上调用的每个办法都会造成调用线程从托管代码转换成本地用户代码,再转换为本地内核模式代码,这些转换须要大量的CPU工夫,如果常常执行可能会对应用程序的性能造成负面影响。

内核模式结构的长处
  • 在资源竞争时,Windows会阻塞输掉的线程,让它不占用CPU从而节约处理器资源
  • 在内核模式结构上阻塞的线程能够指定超时值,如果指定工夫内拜访不到心愿失去的资源,线程能够解除阻塞执行其它工作
  • 一个线程能够始终阻塞,直到一个汇合中的所有内核模式的结构皆可应用或者一个汇合中的任何内核模式的结构可用
通过内核结构实现一个单实例应用程序
static void Main(){    bool createdNew;    //创立一个具备指定名称的内核对象    using (new Semaphore(0, 1, "MyObject", out createdNew))    {        if (createdNew)        {            //线程创立了内核对象,所以必定没有这个应用程序的其它实例正在运行        }        else        {            //线程关上了一个现有的内核对象,阐明实例正在被应用,立刻退出        }    }}
代码解析

假如过程的两个实例同时启动。每个过程都有本人的线程,两个线程都尝试创立具备雷同字符串名称“MyObject”的一个Semaphore。Windows内核确保只有一个线程创立具备指定名称的内核对象。创建对象的线程会将它的createdNew设置为true。

第二个线程,Windows发现具备指定名称的内核对象曾经存在了,因而不容许第二个线程创立另一个同名的内核对象,然而却能够拜访和第一个过程的线程所拜访的一样的内核对象。不同过程的线程便是这样通过一个内核对象相互通信的。在上述代码中第二个线程发现createdNew变量为false,所以晓得这个过程的另一个实例正在运行,所以过程的第二个实例立刻退出。

Event结构

事件是由内核保护的Boolean变量,如果事件为false,在事件上期待的线程就阻塞,反之解除阻塞。事件分为主动重置事件和手动重置事件,当主动重置事件为true时,只唤醒一个阻塞的线程,因为在解除第一个线程的阻塞后,内核将事件重置回false。当手动重置事件为true时,会解除正在期待的所有线程的阻塞,因为内核不将事件主动重置为false,代码必须将事件手动重置回false。

应用主动同步事件创立线程同步锁
class WaitLock : IDisposable{     private AutoResetEvent m_Resources = new AutoResetEvent(true);    public void Enter()    {        //在内核中阻塞,期待资源可用而后返回        this.m_Resources.WaitOne();    }    public void Leave()    {        //开释资源        this.m_Resources.Set();    }    public void Dispose()    {        this.m_Resources.Dispose();    }}
SpinLock与WaitLock性能比照
static void Method() { }static void Main(){    var x = 0;    var iteration = 10000000;    //x递增1000万须要破费工夫    Stopwatch sw = Stopwatch.StartNew();    for (int i = 0; i < iteration; i++)        x++;    Console.WriteLine("x递增1000万次破费工夫: {0}", sw.ElapsedMilliseconds);    //x递增1000万次加上调用一个空办法须要破费的工夫    sw.Restart();    for (int i = 0; i < iteration; i++)    {        Method();        x++;    }    Console.WriteLine("x递增1000万次加上调用一个空办法须要破费的工夫: {0}", sw.ElapsedMilliseconds);    //x递增1000万次加上一个无竞争的SpinLock须要破费的工夫    SpinLock spinLock = new SpinLock();    sw.Restart();    for (int i = 0; i < iteration; i++)    {        spinLock.Enter();        x++;        spinLock.Leave();    }    Console.WriteLine("x递增1000万次加上一个无竞争的SpinLock须要破费的工夫: {0}", sw.ElapsedMilliseconds);    //x递增1000万次加上一个无竞争的WaitLock须要破费的工夫    using (var waitLock = new WaitLock())    {        sw.Restart();        for (int i = 0; i < iteration; i++)        {            waitLock.Enter();            x++;            waitLock.Leave();        }        Console.WriteLine("x递增1000万次加上一个无竞争的WaitLock须要破费的工夫: {0}", sw.ElapsedMilliseconds);    }    Console.ReadKey();}
运行后果

能够看出SpinLock和WaitLock的行为完全相同,然而两个锁的性能齐全不同。锁下面没有竞争的时候WaitLock比SpinLock慢得多,因为下面说到的WaitLock的Enter和Leave办法的每一次调用都强制调用线程从托管代码转换成内核代码。但在存在竞争的时候,输掉的线程会被内核阻塞,不会造成自旋,这是好的中央。

通过例子能够看出内核结构速度慢得可怕,所以须要进行线程同步的时候尽量应用用户模式的结构。

Semaphore结构

信号量(Semaphore)是由内核保护的Int32变量,信号量为0时,在信号量上期待的线程会阻塞。信号量大于0时,就会解除阻塞。在一个信号量上期待的一个线程解除阻塞时,内核主动从信号量的计数中减1。以后信号量计数不能超过信号量关联的最大计数值。

Event结构与Semaphore结构比照
  • 主动重置事件:多个线程在一个主动重置事件上期待时,设置事件只导致一个线程被解除阻塞
  • 手动重置事件:多个线程在一个手动重置事件上期待时,设置事件会导致所有线程被解除阻塞
  • Semaphore结构:多个线程在一个信号量上期待时,开释信号量导致导致releaseCount(开释信号量个数)个线程被解除阻塞(releaseCount是传给Semaphore的Release办法的实参)

一个主动重置事件在行为上和最大计数为1的信号量十分类似,两者的区别就在,能够在一个主动重置事件上间断屡次调用Set,同时依然只有一个线程被解除阻塞。而在一个信号量上间断屡次调用Release,会使它外部的计数始终递增,这可能造成解除大量线程的阻塞。而当计数超过最大计数时,Release会抛出SemaphoreFullException。

示例代码
class SemaphoreLock : IDisposable{    private Semaphore m_Resources;    public SemaphoreLock(int coumaximumConcurThreads)    {        this.m_Resources = new Semaphore(coumaximumConcurThreads, coumaximumConcurThreads);    }    public void Enter()    {        //在内核中阻塞,期待资源可用而后返回        this.m_Resources.WaitOne();    }    public void Leave()    {        //开释资源        this.m_Resources.Release();    }    public void Dispose()    {        this.m_Resources.Close();    }}
Mutex(互斥锁)结构

互斥锁的逻辑
首先Mutex对象会查问调用线程的int ID,记录是哪一个线程取得了锁。一个线程调用ReleaseMutex时,Mutex确保调用线程就是获取Mutex的那个线程。如果不是,Mutex对象的状态就不会扭转,同时ReleaseMutex也会抛出异样ApplicationException。

其次如果领有Mutex的线程终止,那么Mutex上期待的一些线程会因为抛出一个AbandonedMutexException异样而被唤醒,通常该异样也会成为未解决异样。

Mutex对象还保护着一个递归计数,它指明领有该Mutex的线程领有了它多少次。如果一个线程以后领有一个Mutex,而后该线程再次在Mutex上期待,递归计数将递增,且不会阻塞线程,容许这个线程继续执行。线程调用ReleaseMutex时,递归计数递加。只有在递归计数变成0时,另一个线程能力获取该Mutex。

Mutex的毛病

须要更多的内存包容额定的线程ID和递归计数信息,Mutex代码还得保护这些信息,这些都会让锁变得更慢。

递归锁
class SomeType : IDisposable{    private readonly Mutex m_Lock = new Mutex();    public void M1()    {        this.m_Lock.WaitOne();        //do something...        M2(); //递归获取锁        this.m_Lock.ReleaseMutex();    }    public void M2()    {        this.m_Lock.WaitOne();        //do something...        this.m_Lock.ReleaseMutex();    }    public void Dispose()    {        this.m_Lock.Dispose();    }}

SomeType对象调用M1获取一个Mutex,而后调用M2,因为Mutex对象反对递归,所以线程会获取两次锁,而后开释两次,之后另一个线程能力领有它。

内核结构可用时回调办法

让一个线程不确定地期待一个内核对象进入可用状态,这对线程的内存资源来说是一种节约,因而线程池提供了一种形式,在一个内核对象变得可用时回调一个办法。

示例代码
class RegisterdWaitHandleClass{    public static void Main()    {        //结构主动重置事件        AutoResetEvent autoResetEvent = new AutoResetEvent(false);        //通知线程池在AutoResetEvent上期待        RegisteredWaitHandle rwh = ThreadPool.RegisterWaitForSingleObject(            autoResetEvent, //在此事件上期待            EventOperation, //回调EventOperation办法            null, //向EventOperation传递null            5000, //等5s事件变为True            false //每次事件变为True时都调用EventOperation        );        var operation = (char)0;        while(operation != 'Q')        {            operation = char.ToUpper(Console.ReadKey(true).KeyChar);            if (operation == 'S')                autoResetEvent.Set();        }        //勾销注册        rwh.Unregister(null);    }    //任何时候事件为True,或者自从上一次回调超过5s,就调用这个办法    private static void EventOperation(object state, bool timedOut)    {        Console.WriteLine(timedOut ? "超时" : "事件为True");    }}
运行后果(每隔5s输入超时,键盘按下S输入事件为True)