【C#】C#线程_基元线程的同步构造
- 线程同步相关书籍资源:
- 《c#经典实例》线程、同步、并发章节
- 《.net40.面向对象编程漫谈》线程同步章节
- 《c#本质论》
- 《c#高级编程》基础》任务、线程、和同步章节
-
首先先对各种锁来一个概括和总结:
Monitor(lock的底层本身是Monitor来实现的,所以Monitor可以实现lock的所有功能。)
- 如果不能获得锁,则必须等待,线程状态会由运行转换为阻塞(将会出现上下文切换,如果为了保护i++这种访问时间很短的操作,会对程序的性能有损失)
- 相比Lock主要优点是:可以添加一个等待被锁定的超时值。这样就不会无限期地等待被锁定。防止出现死锁。
Lock(Monitor有TryEnter的功能,可以防止出现死锁的问题,lock没有)
- 注意:小心选择lock的对象
- 要声明为私有和只读(只读保证确保在Monitor Enter和Exit之间其值不会发生改变,私有是为了确保类外的同步块不能在同一个对象实例上同步,造成代码阻塞)、
- 避免锁定this、typeof(type)、string(具体见《c#本质论》第3版579页)
- 避免使用MethodImplAttribute同步(具体见《c#本质论》第3版579页)
- 用户模式-同步机制(轮训CPU,不用上下文切换,合适等待时间短的操作)
- 易变构造(Volatile【不稳定的】,使用场景:避免编译器或者CPU对代码进行优化时对多线程产生的影响)
- 互锁构造(Interlocked主要用于操作Int32值,在简单数据类型的变量上执行原子性的读或写操作)
- 线程简单的自旋锁(原子性地操作类对象中的一组字段)【Interlocked.Exchange】
- SpinLock自旋锁:(某个线程可以获取该锁,其他线程此时要获取该锁就会自旋(浪费CPU周期),多核CPU环境下可以提升程序性能,功能与Lock一样)
- Interlocked Anything模式
-
内核模式构造(需要上下文切换、消耗操作系统资源)
-
EventWaitHandle构造
Semaphore构造(信号量--信号灯---一次可以放行多辆车通过)
- 和Monitor一样可以在不同进程间共享:与Mutex一样,也可以指定信号量名称,以便在不同的进程间共享,未命名的信号量只能在本进程中使用。
- 和Mutex的区别:信号量非常类似于Mutex互斥,但可以由个线程使用
- 信号量的定义:使用信号量(第2个参数)定义允许同时访问受锁定保护的资源的线程个数(如果信号量计数是3,则第4个任务必须等待)
-
Mutex构造(互斥锁\互斥量):.NET Framework提供的跨多个进程同步访问的一个类。
- 类似Monitor,只有一个线程能拥有锁定,只有一个线程能获得互斥锁定(派生自WaitHandle)
- 可以在不同进程间共享:构造参数:第2个参数指定互斥名称,没有指定表示“未命名的”互斥只能在本进程中使用。(可以在一个进程中定义互斥的名称,由不同的进程共享)
- 场景:系统能够识别有名称的互斥,因此可以使用它禁止应用程序启动两次
- 内核模式锁
- ManualResetEvent
- Semaphore
- ReaderWriterLock(
- 作用:由于锁 ( lock 和 Monitor ) 是线程独占式访问的,所以其对性能的影响还是蛮大的,ReaderWriterLock:允许多个线程同时读数据、只允许一个线程写数据.
- 场景:常见的用法可能是控制对内存中数据结构的访问,这些数据结构不能自动更新,并且在更新完成之前无效(不应被其他线程读取)。比如对系统中缓存的读写操作【参考Nop.Core.Caching.NopCommerce中PerRequestCacheManager类】)
- 推荐使用.Net4.0中轻量级版本同步方式(使用方法完全一样,但这些类在内部不使用操作系统的核心对象,避免了内核和用户模式的切换,所以性能很高)
- ManualResetEventSlim(混合模式)
- SemaphoreSlim(混合模式)
- ReaderWriterLockSlim(混合模式)
Mutex和Monitor、Lock其他两者的区别
三个都是在限制线程之外的互斥,线程之内,都不限制,同一个线程如果被lock两次。是不会出现死锁的。所以Mutex本身可以实现lock和Monitor所有的操作。至少从功能上讲是这样的。
但是Mutex是内核级别的,消耗较大的资源,不适合频繁的操作,会降低操作的效率。所以一般被调用部分的资源锁,常常用lock或者Monitor,可以提高效率。而线程和线程间的协调,可以用Mutex,因为相互互斥切换的机会会大大的降低,效率就不再那么的重要了。
Mutex本身是可以系统级别的,所以是可以跨越进程的。比如我们要实现一个软件不能同时打开两次,那么Mutex是可以实现的,而lock和monitor是无法实现的。
可以参考:https://www.cnblogs.com/xiashengwang/archive/2012/08/31/2664225.html(混合线程同步核心篇)
1.1 为什么需要使用线程同步(多路进军,排除依次过桥【不然桥会倒掉,或者人掉河里】)
当多个线程同步访问共享数据时,可能会出现数据损坏的现象,使用线程同步可以防止线程损坏。虽然线程同步防止了数据的损坏,然而这并不是没有代价的。如果一些数据由两个线程同时访问,那么那些不可能访问到这些数据的线程就更本不需要进行线程同步。
一些常见的场景:
- 文件的读写操作(图片、其他文件等)
- 对缓存的读写操作
- 商品库存增、减(抢购、秒杀)
- 计数器
- 出票系统,票务处理、
- 抽奖系统的奖品库存
1.2 线程同步的缺点(避免线程同步)
不需要线程同步是最理想的情况,因为线程同步存在许多问题。
第一,线程同步比较繁琐,而且容易写错。在代码中必须标识出所有可能会被多个线程访问的数据,并且用额外的代码包围起来,并获取和释放一个同步锁。
第二,线程同步会损害性能。获取和释放锁都是需要时间的,因为要调用一些额外的方法,而且不同的CPU必须进行协调,以决定哪个线程先取得锁。
第三,线程同步通常只允许一个线程访问资源,这是同步锁的意义所在,但也是问题所在,因为阻塞一个线程通常会创建更多的线程。假如,一个线程池线程试图获取一个暂时无法获取到的锁,线程池就有可能会创建新的线程(如果线程池还有其他的任务需要执行),使CPU继续执行其他的任务。如果创建了大量的额外线程,那么还会造成CPU频繁切换上下文,更影响性能(这一点也是为什么要异步函数的原因)。
线程同步是一件不好的事,所以在设计应用程序时候,应该尽量避免使用线程同步。如果一个共享数据必须要由多个线程访问,那么可以考虑使用值类型,因为值类型总是被复制,每个操作线程都有其副本。
2 基元线程同步
2.1 什么是基元线程同步
这里解释一下什么基元,基元就是指可以在代码中使用的最简单的构造。基元线程同步就是指在线程中使用最简单的同步方式构造线程同步。基元有两种构造模式:用户模式构造和内核模式构造(
- c# 基元类型中的基元:编译器直接支持的数据类型称为基元类型。基元类型直接映射到Framework类库(FCL)中存在的类型。比如在c#中int直接映射到System.Int32类型)
.NET4.0之前为我们提供的各种同步基元(包括Interlocked、Monitor\lock、EventWaitHandle、Mutex、Semaphore等),随着.NET框架的进化,.NET4.0|.NET4.5又为我们带来了更多优化的同步基元选择。这当然不是告诉我们完全放弃.NET4.0之前所提供的同步基元,只是需要我们“因地制宜”。那我们如何判断适合使用哪种同步基元结构呢,就需要我们对各种同步基元有个本质的理解和清楚.NET所做的优化本质是什么。
线程同步的2种构造模式:
- 基元用户模式构造
- 基元内核模式构造
- 混合构造(结合以上2种方式)
相关资源:https://www.cnblogs.com/heyuquan/archive/2013/01/10/2854311.html
2.2 基元用户模式构造和内核模式构造的比较
这两种构造模式中,基元用户模式的构造明显比内核模式构造的效率高。下面将会分析其优缺点。
-
基元用户模式构造(积极、主动的线程小兵、效率高,对CPU不利)
发现锁:线程持续运行,轮询并占用着CPU,直到拿到锁(活锁、自旋)
优点:使用特殊的CPU指令来调节线程,即协调是在硬件中发生的,速度很快。并且在用户模式的基元构造上阻塞的线程池永远不会认为其阻塞,所以线程池不会创建新的线程来替换临时的阻塞,同时,这些CPU指令只阻塞相当短的一段时间。缺点:只有Windows操作系统内核(内核是操作系统最基本的部分)才能停止一个线程的运行,然而在用户模式中运行的线程可能会被抢占,所以想要取得资源但是取不到资源的线程,就可能会一直“自旋”,这可能会浪费大量的CPU时间。
举个例子来模拟一下用户模式构造的同步方式:
- 线程1请求了临界资源,并在资源门口使用了用户模式构造的锁;
- 线程2请求临界资源时,发现有锁,因此就在门口等待,并不停的去询问资源是否可用;
- 线程1如果使用资源时间较长,则线程2会一直运行,并且占用CPU时间。占用CPU干什么呢?她会不停的轮询锁的状态,直到资源可用,这就是所谓的活锁;“自旋”
缺点有没有发现?线程2会一直使用CPU时间,浪费cpu资源(假如当前系统只有这两个线程在运行),也就意味着不仅浪费了CPU时间,而且还会有频繁的线程上下文切换,对性能影响是很严重的。
当然她的优点是效率高,适合哪种对资源占用时间很短的线程同步。.NET中为我们提供了两种原子性操作,利用原子操作可以实现一些简单的用户模式锁(如自旋锁)。
System.Threading.Interlocked:互锁构造,它在包含一个简单数据类型的变量上执行原子性的读或写操作。(Inter进入)
Thread.VolatileRead 和 Thread.VolatileWrite:易失(不稳定)构造,它在包含一个简单数据类型的变量上执行原子性的读和写操作。
-
内核模式构造(一边待着去!啥时候叫你,你再来!(被动等待)。效率低,但对CPU有利 )
发现锁:操作系统主动要求线程睡眠,解锁后:再来唤醒线程(阻塞,死锁)
优点:当线程通过内核模式获取其他线程的资源时,如果资源不可用,内核会阻止这个线程使其阻塞(这样就不会浪费CPU时间),当资源可用时候,Windows内核会恢复这个线程,允许它访问资源。缺点:因为内核模式构造是由Windows操作系统自己提供的函数来实现的,所以,当应用程序调用Windows自身的函数时,线程会从用户模式切换为内核模式(或相反)会导致巨大的性能损失,这是正是为什么要避免使用内核模式的原因。
模拟一个内核模式构造的同步流程来理解她的工作方式:
- 线程1请求了临界资源,并在资源门口使用了内核模式构造的锁;
- 线程2请求临界资源时,发现有锁,就会被系统要求睡眠(阻塞),线程2就不会被执行了,也就不会浪费CPU和线程上下文切换了;
- 等待线程1使用完资源后,解锁后会发送一个通知,然后操作系统会把线程2唤醒。假如有多个线程在临界资源门口等待,则会挑选一个唤醒;
看上去是不是非常棒!彻底解决了用户模式构造的缺点,但内核模式也有缺点的:将线程从用户模式切换到内核模式(或相反)导致巨大性能损失。调用线程将从托管代码转换为内核代码,再转回来,会浪费大量CPU时间,同时还伴随着线程上下文切换,因此尽量不要让线程从用户模式转到内核模式。
她的优点就是阻塞线程,不浪费CPU时间,适合那种需要长时间占用资源的线程同步。
内核模式构造的主要有两种方式,以及基于这两种方式的常见的锁:
- 基于事件:如AutoResetEvent、ManualResetEvent
- 基于信号量:如Semaphore
如果能够集合内核模式和基元用户模式构造的优点创建一个新的构造模式就好了,这种构造模式确实存在,其名为“混合构造”。本文重点是介绍基元线程的构造。混合构造将会留在后面的文章中介绍。
活锁:如果在一个构造上等待的线程一直获取不到想要的资源并且这是一个用户模式的构造,线程一直在一个CPU上运行,称为活锁
死锁:如果在一个构造上等待的线程一直获取不到想要的资源并且这是一个内核模式的构造,线程一直阻塞,称为死锁
*死锁总是优于活锁,因为活锁既浪费CPU时间,又浪费内存,而死锁只浪费内存
相关资源:
- http://www.mamicode.com/info-detail-1370935.html
- https://docs.microsoft.com/zh-cn/dotnet/api/system.threading.interlocked?view=netframework-4.8
- https://www.cnblogs.com/yaopengfei/p/8315212.html
- https://www.cnblogs.com/wangyonglai/p/8241724.html
3.用户模式构造
Windows保证对以下数据类型的变量读写是原子性的:Boolean,Char,(S)Byte,(U)Int16,(U)Int32,(U)IntPtr,Single以及引用类型。这意味着变量中的所有字节都是一次性读取或写入。
例如:
Int32 x=0;
如果有个线程执行如下代码:
x=0x012345;
那么线程会一次性(原子性)地从0变成0x012345。另一个线程不可能看到处于中间状态的值。
.NET Framework提供了如下的简单用户模式构造:易变构造和互锁构造。下面将会一一介绍:
3.1 易变构造(Volatile)
先来看看如下的代码:
internal static class StrangeBehaviour { private static volatile Boolean s_stopWorker = false; public static void Main(String[] args) { Console.WriteLine("Main: letting worker run for 5 seconds"); Thread t = new Thread(Worker); t.Start(); Thread.Sleep(5000); s_stopWorker = true; Console.WriteLine("Main:waiting for worker to stop"); t.Join(); Console.ReadLine(); } private static void Worker(Object o) { Int32 x = 0; while (!s_stopWorker) x++; Console.WriteLine("Worker: stopped when x={0}",x); } }
如果以调试方式的运行(或是不加上优化运行),那么程序将会按照期望的那样结束。如果加上优化的话,那么结局将会不同,打开C#编译器的 platform:x86 和 /optimize+ 开关来编译,然后运行,会发现程序一直运行,并不会停止。
接下来解释其过程,当Worker方法开始工作时,s_stopWorker是false,程序进入循环,在5秒钟后,主线程改变s_stopWorker为true,所以Worker方法中的线程应该立即停止。然而,因而加了优化,所以每次当while(!s_stopWorker)执行时,并不会去检查s_stopWorker的真实值,只检查一次(也就是在循环开始的时候),之后其会记住s_stopWorker的值,本例中为false,所以程序会一直执行。
System.Threading.Volatile类提供了如下类似静态方法(有许多的重载方法):
public static class Volatile{ public static void Write(ref Int32 location,Int32 value) public static Int32 Read(ref Int32 location) public static void Write(ref Boolean location,Boolean value) public static Int32 Read(ref Boolean location) .... }
他们会禁止C#编译器,JIT编译器和CPU平常执行的一些优化。他们总是对变量本身进行直接操作,而不是对其中间值进行操作。换句话说,使用Volatile对字段操作,总是直接对RAM进行操作,而不会对CPU寄存器进行操作(不使用CPU寄存器,速度肯定会比使用CPU寄存器慢)。
可以像这样重写上面的方法:
internal static class StrangeBehaviour { private static volatile Boolean s_stopWorker = false; public static void Main(String[] args) { Console.WriteLine("Main: letting worker run for 5 seconds"); Thread t = new Thread(Worker); t.Start(); Thread.Sleep(5000); Volatile.Write(ref s_stopWorker,true); Console.WriteLine("Main:waiting for worker to stop"); t.Join(); Console.ReadLine(); } private static void Worker(Object o) { Int32 x = 0; while (!Volatile.Read(ref s_stopWorker)) x++; Console.WriteLine("Worker: stopped when x={0}",x); } }
除了可以使用Volatile的Read和Write方法对线程共享数据进行操作,也可以使用volatile关键字,使用volatile关键字修饰字段时候,告诉编译器读取数据时候总是以Volatile.Read方式读取,写数据时总是以Volatile.Write方法写入。
Volatile除了可以禁用编译器的一些优化,还建立完整的内存栅栏。具有如下规则:
1,对于共享内存,所有写操作都保证了在Volatile.Write之前完成。
2,对于共享内存,所有读操作都保证了在Volatile.Read之后完成。
是不是比较空洞,我们来看看如下这个案例:
class Program { static void Main(string[] args) { Memorybarrier memorybarrier = new Memorybarrier(); new Thread(memorybarrier.method1).Start(); new Thread(memorybarrier.method2).Start(); } } class Memorybarrier { public int i = 0; public int j = 0; public void method1() { i = 1; j = 10; } public void method2() { if (i == 1) { Console.Write("j="+j); } } }
上面可能会输出:
j=0
我们来分析,当没有建立内存栅栏时候,以method2方法为例,在赋值的时候,很有可能先给要使用j内存赋值,然后再给i的内存赋值,比如:先给j的内存赋值为0,然后method1方法运行i赋值为1,然后method2恢复运行,得到i为1,并且打印j的值。最后method1方法恢复运行,赋值j为10。这显然是不符合我们的逻辑的,而且可控性不强。上面有两个变量在两个线程中使用,我们只需要给i建立完成的内存栅栏,就可以避免这样的情况。
public void method1() { j=10; Volatile.Write(ref i,1); } public void method2() { if (Volatile.Read(ref i)== 1) { Console.Write("j="+j); } }
注意,这里不能只对j建立完整的内存栅栏,因为如果是这样的话,Console.Write("j="+Volatile.Read(ref j));读取内存就是最后了,这样不符合标准,比如:
public void method1() { i=1; Volatile.Write(ref j,10); } public void method2() { if (i== 1) { Console.Write("j="+Volatile.Read(ref j)); } }
上面代码中,仍然可能出现Bug,method1方法没问题,在method2中,因为使用Volatile.Read(ref j)读取内存在i读取内存之后。比如:当method1运行i=1,然后method2运行,if(i==1)为true,然后运行Volatile.Read(ref j),此时method1还没有改变其值,所以打印依然是0,最后method1恢复执行Volatile.Write(ref j,10),这显然是于事无补的。
那么可不可以给所有共享内存的线程都建立完整的内存栅栏呢,这当然是可以的。为了变量赋值的顺序性,牺牲的就是内存和性能。
3.2 互锁构造(Interlocked)
我们已经知道了Volatile的Read和Write操作是分别执行一次原子性的读取和写入操作。Interlocked也是一种可以用来执行原子操作的基元用户模式。
下面代码用来演示Interlocked的方法异步查询几个Web服务器,并同时处理返回的数据。下面的代码很短,而且不会阻塞任何线程:
MutiWebRequest类:
class MutiWebRequest { //辅助类用于协调所有的异步操作 private AsyncCoordinator m_ac = new AsyncCoordinator(); //这是想要查询的Web服务器及其响应(异常或Int32)的集合 //注意:多个线程访问该字典不需要以同步方式进行, //因为构造后键是只读的 private Dictionary<String, Object> m_servers = new Dictionary<string, object>() { {"https://www.wintellect.com/",null}, {"http://Microsoft.com",null}, {"http://baidu.com",null} }; public MutiWebRequest(Int32 timeout = Timeout.Infinite) { //以异步方式发起一次性的所有请求 var http=new HttpClient(); foreach(var server in m_servers.Keys){ m_ac.AboutToBegin(); http.GetByteArrayAsync(server).ContinueWith(task => { ComputeResult(server,task); }); ; } //告诉AsyncCoordinator所有操作都已发起,并在所有操作完成、 //调用Cancel或发生超时的时候调用AllDone m_ac.AllBegun(AllDown, timeout); } private void ComputeResult(String server, Task<Byte[]> task) { Object result; if (task.Exception != null) { result = task.Exception.InnerException; } else { //在线程池上处理I/O完成 //在此添加自己的计算密集型算法.... result = task.Result.Length;//本例只返回长度 } //保存结果 m_servers[server] = result; m_ac.JustEnded(); } //调用这个方法指出结果以无关紧要 public void Cancel() { m_ac.Cancel(); } //所有Web服务器都已经响应,调用了Cancel或者发生了超时 private void AllDown(CoordinationStatus status) { switch (status) { case CoordinationStatus.Cancel: Console.WriteLine("operation canceled"); break; case CoordinationStatus.Timeout: Console.WriteLine("operation timeout"); break; case CoordinationStatus.AllDown: Console.WriteLine("operation completed; result below:"); foreach (var server in m_servers) { Console.Write("{0}",server.Key); Object result = server.Value; if (result is Exception) { Console.WriteLine("failed due to {0}.", result, GetType().Name); } else { Console.WriteLine("returned {0:N0} bytes.",result); } } break; } } }
CoordiationStatus枚举:
enum CoordinationStatus{AllDown,Timeout,Cancel}
AsyncCoordinator类:
class AsyncCoordinator { private Int32 m_opCount = 1; private Int32 m_statusReported = 0; private Action<CoordinationStatus> m_callback = null; private System.Threading.Timer m_timer = null; //该方法在一个操作发起之前调用 public void AboutToBegin(Int32 opsToAdd = 1) { Interlocked.Add(ref m_opCount,opsToAdd); } //该方法在一个操作发起之后调用 public void JustEnded() { if (Interlocked.Decrement(ref m_opCount) == 0) { ReportStatus(CoordinationStatus.AllDown); } } //该方法在所有操作发起后调用 public void AllBegun(Action<CoordinationStatus> callback,Int32 timeout=Timeout.Infinite) { m_callback = callback; if (timeout != Timeout.Infinite) { m_timer = new System.Threading.Timer(TimerExpired, null, timeout, Timeout.Infinite); } JustEnded(); } public void TimerExpired(Object o) { ReportStatus(CoordinationStatus.Timeout); } public void Cancel() { ReportStatus(CoordinationStatus.Cancel); } private void ReportStatus(CoordinationStatus status) { //如果状态从未报告过,就报告 if (Interlocked.Exchange(ref m_statusReported,1) == 0) { m_callback(status); } } }
3.3 线程简单的自旋锁
Interlocked的方法非常好用,但主要用于操作Int32值。如果需要原子性地操作类对象中的一组字段,该怎么办呢?在这种情况下,需要采用一个方法阻止所有线程,只允许其中一个线程进入并对字段进行操作。
例如:
internal struct SimpleSpinLock{ private Int32 m_ResourceInUse;//0=false(HttpClient),1=true public void Enter(){ while(true){ //总是将资源设置为“正在使用”(1) //只有从“未使用”变成“正在使用”才会返回1 if(Interlocked.Exchange(ref m_ResourceInUse,1)==0) return; //在这里添加“黑科技” } } public void Leave(){ //将资源标记为“未使用” Volatile.Write(ref m_ResourceInUse,0); } }
下面展示了如何使用
public sealed class SomeResource{ private SimpleSpinLock m_sl=new SimpleSpinLock(); public void AccessResource(){ m_sl.Enter(); //一次只有一个线程进入 m_sl.Leave(); } }
上面封装的自旋锁SimpleSpinLock并不是很好,当一个线程未获取到资源后,就进入自旋,直到其他资源变得可用。看上面的SimpleSpinLock可以知道,如果一个线程未获取到资源,就会进入疯狂自旋模式,将会耗费大量的CPU时间。
3.3.1 SpinLock自旋锁
上面的SimpleSpinLock提供了一种简单的自旋锁模式,SimpleSpinLock并未加什么特殊的而优化。幸运的是,在FCL中已经给我们提供好了一个System.Threading.SpinWait的结构,它封装了上面SimpleSpinLock中关于“黑科技”的最新研究。FCL还包含了System.Threading.SpinLock结构,它和上面的SimpleSpinLock结构相似,只是使用了SpinWait来增强性能。SpinWait还提供了超时支持。而且SpinWait和SpinLock都是值类型,因此它是轻量级的、内存友好的对象。
3.4 Interlocked Anything模式
Interlocked类并没有提供一组丰富的操作方法,比如Mutilple,Divide,Minimun,Maximum...方法。虽然Interlocked并没有提供这些方法,但一个已知的模式是Interlocked.CompareExchange方法,以原子的方式在Int32上执行任何操作。该方法还提供了丰富的重载版本。该模式类似于修改数据库记录时使用的乐观并发模式。
例如下面是一个原子的Maximun方法:
public static Int32 Maximum(ref Int32 target, Int32 value) { Int32 currentVal = target, startVal, desireVal; do{ //记录这一次循环迭代的起始值(startVal) startVal=currentVal; desireVal=Math.Max(startVal,value); //注意:线程在这里可能会被抢占,所以以下代码不是原子的 //if(target==startVal) target=desireVal; //应该使用下面原子的CompareExchange方法,它返回target在被方法修改之前的值 currentVal=Interlocked.CompareExchange(ref target,desireVal,startVal); //如果target的值在这一次循环中被其他线程改变,就重复 }while(startVal!=currentVal); return desireVal; }
4.内核模式构造
4.1 EventWaitHandle构造
Event其实只是由内核维护的Boolean变量。事件为false,在事件上等待的线程就阻塞;事件为True就解除阻塞。有两种事件,自己重置事件和手动重置事件。当一个自己重置事件为true时,它只唤醒一个阻塞的线程,因为在解除第一个线程的阻塞后,内核自动将事件重置为false,造成其他线程阻塞。而手动重置事件为true时,它解除正在等待它的所有线程的线程,因为内核不将事件自动重置为false;
4.2 Semaphore构造
System.Threading.Semaphore限制对线程的资源访问的数量。Semaphore(信号量)其实就是由内核维护的Int32变量。当信号量为0时,在信号量上等待的线程会阻塞;信号量大于0时候,解除阻塞。在信号量上等待的线程解除阻塞时,内核自动从信号量的计数中减1。信号量还关联了一个最大的Int32值,当前计数绝不允许超过最大计数。
4.3 Mutex构造
Mutex(互斥体)代表一个互斥的锁。它的工作方式和AutoResetEvent(或者计数为1的Semaphore相似),三者都是一次只能释放一个正在等待的线程。
互斥体有一些逻辑,这造成他们比其他构造复杂。首先Mutex对象会调查调用线程的Int32 ID,记录时那个线程获得了它。一个线程在调用ReleaseMutex时,Mutex确保调用线程就是获取Mutex的那个线程。如若不然,Mutex对象的状态就不会改变,而ReleaseMutex会抛出一个System.ApplicationException。
以下为NopCommerce项目中的代码(命名互斥体有助于避免在不同的线程中创建相同的文件,并且不会显著降低性能,因为代码只针对特定的文件被阻塞。)
//the named mutex helps to avoid creating the same files in different threads,
//命名互斥体有助于避免在不同的线程中创建相同的文件
//and does not decrease performance significantly, because the code is blocked only for the specific file.
//并且不会显著降低性能,因为代码只针对特定的文件被阻塞。
using (var mutex = new Mutex(false, thumbFileName)) { if (GeneratedThumbExists(thumbFilePath, thumbFileName)) return GetThumbUrl(thumbFileName, storeLocation); mutex.WaitOne();//此方法申请访问共享资源,得到允许后,Mutex对象状态变为owned,这是其他调用Mutex对象WaitOne方法的线程会被阻塞 //check, if the file was created, while we were waiting for the release of the mutex. if (!GeneratedThumbExists(thumbFilePath, thumbFileName)) { pictureBinary = pictureBinary ?? LoadPictureBinary(picture); if ((pictureBinary?.Length ?? 0) == 0) return showDefaultPicture ? GetDefaultPictureUrl(targetSize, defaultPictureType, storeLocation) : string.Empty; byte[] pictureBinaryResized; if (targetSize != 0) { //resizing required using (var image = Image.Load(pictureBinary, out var imageFormat)) { image.Mutate(imageProcess => imageProcess.Resize(new ResizeOptions { Mode = ResizeMode.Max, Size = CalculateDimensions(image.Size(), targetSize) })); pictureBinaryResized = EncodeImage(image, imageFormat); } } else { //create a copy of pictureBinary pictureBinaryResized = pictureBinary.ToArray(); } SaveThumb(thumbFilePath, thumbFileName, picture.MimeType, pictureBinaryResized); } mutex.ReleaseMutex(); }
5.混合锁
Monitor和Lock请参考:
- https://www.cnblogs.com/yaopengfei/p/8315212.html
- https://www.cnblogs.com/waw/archive/2011/09/02/2163092.html
1. 简介:混合锁=用户模式锁+内核模式锁,先在用户模式下内旋,如果超过一定的阈值,会切换到内核锁,在内旋模式下,我们会看到大量的Sleep(0),Sleep(1),Yield等语法。
Thread.Sleep(1) 让线程休眠1ms
Thread.Sleep(0) 让线程放弃当前的时间片,让本线程更高或者同等线程得到时间片运行。
Thread.Yield() 让线程立即放弃当前的时间片,可以让更低级别的线程得到运行,当其他thread时间片用完,本thread再度唤醒。
混合锁包括以下三种:ManualResetEventSlim、SemaphoreSlim、ReaderWriterLockSlim,这三种混合锁,要比他们对应的内核模式锁 (ManualResetEvent、Semaphore、ReaderWriterLock),的性能高的多。
2. ManualResetEventSlim
构造函数默认为false,可以使用Wait方法替代WaitOne方法,支持任务取消. (详细的代码同内核版本类似,这里不做测试了)
3. SemaphoreSlim
用法和内核版本类似,使用Wait方法代替WaitOne方法,Release方法不变。(详细的代码同内核版本类似,这里不做测试了)
4. ReaderWriterLockSlim
用法和内核版本类似,但是四个核心方法换成了:
锁读的两个核心方法:EnterReadLock、ExitReadLock。
锁写的两个核心方法:EnterWriteLock、ExitWriteLock。
6.基元用户模式和内核模式性能比较
在上面的笔者已经说过基元用户模式的性能要优于内核模式,接下来笔者选择基元用户模式中的System.Threading.SpinLock自旋锁,和内核模式中的System.Threading.AutoResetEvent事件锁进行比较。
class Program { static void Main(string[] args) { Int32 x = 0; const Int32 iterations = 10000000; Stopwatch sw = new Stopwatch(); //X递增1000万次 for (Int32 i = 0; i < iterations; i++) { x++; } Console.WriteLine("Incrementing x : {0:N}",sw.ElapsedMilliseconds); x = 0; sw.Restart(); //X递增1000万次,加上调用一个什么都不做方法的开销 for (Int32 i = 0; i < iterations; i++) { M(); x++; M(); } Console.WriteLine("Incrementing x in M :{0:N}",sw.ElapsedMilliseconds); x = 0; SpinLock sl = new SpinLock(); Boolean locktoken = false; sw.Restart(); //X递增1000万次,加上调用一个无竞争的SpinLock开销 for (Int32 i = 0; i < iterations; i++) { locktoken = false; sl.Enter(ref locktoken); x++; sl.Exit(); } Console.WriteLine("Incrementing x in SpinLock : {0:N}", sw.ElapsedMilliseconds); x = 0; AutoResetEvent autoresetevent = new AutoResetEvent(true); sw.Restart(); //X递增1000万次,加上调用一个无竞争的AutoResetEvent开销 for (Int32 i = 0; i < iterations; i++) { autoresetevent.WaitOne(); x++; autoresetevent.Set(); } Console.WriteLine("Incrementing x in AutoResetEvent : {0:N}", sw.ElapsedMilliseconds); Console.ReadLine(); } static void M() { //该方法什么都不做 } } //笔者平台的结果如下: ///result: ///Incrementing x : 0.00 ///Incrementing x in M :60.00 ///Incrementing x in SpinLock : 2,537.00 ///Incrementing x in AutoResetEvent : 12,757.00
单纯的递增几乎不需要花费时间,但是在调用前后加上一个空方法就要将近多花费60倍的时间,使用基元用户模式的自旋锁就要将近多花费42(6537/60)倍,如果使用内核模式又要多花费5(12757/2537)倍的时间。