多个线程试图同时访问同一个数据时,数据不会遭到破坏缓存
构造模式分别有用户模式和内核模式两种,其中用户模式构造使用了特殊的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对代码进行优化的时候,从单线程的角度看,代码会作咱们但愿它作的事情,而从多线程来看,代码的意图不必定会获得保留,如下的代码进行了演示:this
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的状况。spa
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的值。操作系统
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中进行。pwa
调用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时间,若是常常执行可能会对应用程序的性能形成负面影响。
static void Main() { bool createdNew; //建立一个具备指定名称的内核对象 using (new Semaphore(0, 1, "MyObject", out createdNew)) { if (createdNew) { //线程建立了内核对象,因此确定没有这个应用程序的其它实例正在运行 } else { //线程打开了一个现有的内核对象,说明实例正在被使用,当即退出 } } }
假设进程的两个实例同时启动。每一个进程都有本身的线程,两个线程都尝试建立具备相同字符串名称“MyObject”的一个Semaphore。Windows内核确保只有一个线程建立具备指定名称的内核对象。建立对象的线程会将它的createdNew设置为true。
第二个线程,Windows发现具备指定名称的内核对象已经存在了,所以不容许第二个线程建立另外一个同名的内核对象,可是却能够访问和第一个进程的线程所访问的同样的内核对象。不一样进程的线程即是这样经过一个内核对象互相通讯的。在上述代码中第二个线程发现createdNew变量为false,因此知道这个进程的另外一个实例正在运行,因此进程的第二个实例当即退出。
事件是由内核维护的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(); } }
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)是由内核维护的Int32变量,信号量为0时,在信号量上等待的线程会阻塞。信号量大于0时,就会解除阻塞。在一个信号量上等待的一个线程解除阻塞时,内核自动从信号量的计数中减1。当前信号量计数不能超过信号量关联的最大计数值。
一个自动重置事件在行为上和最大计数为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对象会查询调用线程的int ID,记录是哪个线程得到了锁。一个线程调用ReleaseMutex时,Mutex确保调用线程就是获取Mutex的那个线程。若是不是,Mutex对象的状态就不会改变,同时ReleaseMutex也会抛出异常ApplicationException。
其次若是拥有Mutex的线程终止,那么Mutex上等待的一些线程会由于抛出一个AbandonedMutexException异常而被唤醒,一般该异常也会成为未处理异常。
Mutex对象还维护着一个递归计数,它指明拥有该Mutex的线程拥有了它多少次。若是一个线程当前拥有一个Mutex,而后该线程再次在Mutex上等待,递归计数将递增,且不会阻塞线程,容许这个线程继续执行。线程调用ReleaseMutex时,递归计数递减。只有在递归计数变成0时,另外一个线程才能获取该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"); } }