编程编的久了,总会遇到多线程的情况,有些时候我们要几个线程互助完成某些功能,这时候可以定义一个全局对象,各个线程根据这个对象的状态来协同工作,这就是根本的线程同步。
支持多线程编程的语言一般都内置了一些范例和方法用于创建上述所说的全局对象也就是锁对象,它们的作用雷同,使用场景有所差别。.Net中这玩意儿有很多,若不是经常使用,我想没人能完全记住它们各自的用法和相互的区别。为了便于查阅,现将它们记录在此。
ps:本文虽然关注 .Net 平台,但涉及到的大部分锁概念都是平台无关的,在很多其它语言(如_Java__)中都能找到对应。_
volatile 关键字
确切地说,volatile 并不属于锁的范畴,但其背后蕴藏着多线程的根本概念,偶然人们也使用它实现自定义锁。
缓存一致性
了解volatile,起首要了解.Net/Java的内存模型(.Net 当年是诸多鉴戒了 Java 的筹划理念)。而 Java 内存模型又鉴戒了硬件层面的筹划。
我们知道,在今世计算机中,处置处罚器的指令速率远超内存的存取速率,所以今世计算机系统都不得不参加一层读写速率尽大概接近处置处罚器运算速率的高速缓存来作为主存与处置处罚器之间的缓冲。处置处罚器计算直接存取的是高速缓存中的数据,计算完毕后再同步到主存中。
在多处置处罚器系统中,每个处置处罚器都有本身的高速缓存,而它们又共享同一主存。
而 Java 内存模型的每个线程有本身的工作内存,此中保留了被线程使用的变量的副本。线程对变量的全部的操作都必须在工作内存中完成,而不能直接读写主内存中的变量。差别线程之间也不能直接访问对方工作内存中的变量,线程间变量的值的传递需要通过主内存中转来完成。
虽然两者的筹划相似,但是前者主要解决存取服从不匹配的题目,而后者主要解决内存安全(竞争、泄漏)方面的题目。显而易见,这种筹划方案引入了新的题目——缓存一致性(CacheCoherence)——即各工作内存、工作内存与主存,它们存储的相同变量对应的值大概不一样。
为了解决这个题目,很多平台都内置了 volatile 关键字,使用它修饰的变量,可以包管全部线程每次获取到的是最新值。这是怎么做到的呢?这就要责备部线程在访问变量时遵循预定的协议,比如MSI、MESI(IllinoisProtocol)、MOSI、Synapse、Firefly及DragonProtocol等,此处不赘述,只需要知道系统额外帮我们做了一些事情,多少会影响执行服从。
另外 volatile 还能制止编译器自作聪明重排指令。重排指令在大多数时候无伤大雅,还能对执行服从有一定提升,但某些时候会影响到执行结果,此时就可以使用 volatile。
Interlocked
同 volatile 的可见性作用雷同,Interlocked 可为多个线程共享的变量提供原子操作,这个类是一个静态类,它提供了以线程安全的方式递增、递减、交换和读取值的方法。
它的原子操作基于 CPU 本身,非壅闭,所以也不是真正意义上的锁,当然服从会比锁高得多。
锁模式
接下来正式介绍各种锁之前,先了解下锁模式——锁分为内核模式锁和用户模式锁,后面也有了混合模式锁。
内核模式就是在系统级别让线程制止,收到信号时再切回来继续干活。该模式在线程挂起时由系统底层负责,几乎不占用 CPU 资源,但线程切换时服从低。
用户模式就是通过一些 CPU 指令大概死循环让线程一直运行着直到可用。该模式下,线程挂起会一直占用 CPU 资源,但线程切换非常快。
长时间的锁定,优先使用内核模式锁;假如有大量的锁定,且锁定时间非常短,切换频繁,用户模式锁就很有用。另外内核模式锁可以实现跨进程同步,而用户模式锁只能进程内同步。
本文中,除文末轻量级同步原语为用户模式锁,其它锁都为内核模式。
lock 关键字
lock 应该是大多数开辟职员最常用的锁操作,此处不赘述。需要注意的是使用时应 lock 范围尽量小,lock 时间尽量短,制止无谓期待。
Monitor
上面 lock 就是Monitor的语法糖,通过编译器编译会生成 Monitor 的代码,如下:- lock (syscRoot)
- {
- //synchronized region
- }
- //上面的lock锁等同于下面Monitor
- Monitor.Enter(syscRoot);
- try
- {
- //synchronized region
- }
- finally
- {
- Monitor.Exit(syscRoot);
- }
复制代码 Monitor 还可以设置超时时间,制止无限制的期待。同时它另有 Pulse\PulseAll\Wait 实现唤醒机制。
ReaderWriterLock
很多时候,对资源的读操作频率要远远高于写操作频率,这种情况下,应该对读写应用差别的锁,使得在没有写锁时,可以并发读(加读锁),在没有读锁或写锁时,才可以写(加写锁)。ReaderWriterLock就实现了此功能。
主要的特点是在没有写锁时,可以并发读,而非一概而论,不论读写都只能一次一个线程。
MethodImpl(MethodImplOptions.Synchronized)
假如是方法层面的线程同步,除上述的lock/Monitor之外,还可以使用MethodImpl(MethodImplOptions.Synchronized)特性修饰目标方法。
SynchronizationAttribute
ContextBoundObject
要了解SynchronizationAttribute,不得不先说说ContextBoundObject。
起首进程中承载步伐集运行的逻辑分区我们称之为AppDomain(应用步伐域),在应用步伐域中,存在一个或多个存储对象的区域我们称之为Context(上下文)。
在上下文的接口当中存在着一个消息接收器负责检测拦截和处置处罚信息。当对象是MarshalByRefObject的子类的时候,CLR将会创建Transparent Proxy,实现对象与消息之间的转换。应用步伐域是 CLR 中资源的边界。一般情况下,应用步伐域中的对象不能被外界的对象所访问,而MarshalByRefObject 的功能就是允许在支持远程处置处罚的应用步伐中跨应用步伐域边界访问对象,在使用.NET Remoting远程对象开辟时经常使用到的一个父类。
而ContextBoundObject更进一步,它继承 MarshalByRefObject,即使处在同一个应用步伐域内,假如两个 ContextBoundObject 所处的上下文差别,在访问对方的方法时,也会借由Transparent Proxy实现,即接纳基于消息的方法调用方式。这使得 ContextBoundObject 的逻辑永远在其所属的上下文中执行。
ps: 相对的,没有继承自 ContextBoundObjec t的类的实例则被视为上下文灵活的(context-agile),可存在于恣意的上下文当中。上下文灵活的对象总是在调用方的上下文中执行。
一个进程内可以包括多个应用步伐域,也可以有多个线程。线程可以穿梭于多个应用步伐域当中,但在同一个时候,线程只会处于一个应用步伐域内。线程也能穿梭于多个上下文当中,进行对象的调用。
SynchronizationAttribute用于修饰ContextBoundObject,使得其内部构成一个同步域,同一时段内只允许一个线程进入。
WaitHandle
在查阅一些异步框架的源码或接口时,经常能看到WaitHandle这个东西。WaitHandle 是一个抽象类,它有个焦点方法WaitOne(int millisecondsTimeout, bool exitContext),第二个参数表示在期待前退出同步域。在大部分情况下这个参数是没有用的,只有在使用SynchronizationAttribute修饰ContextBoundObject进行同步的时候才有用。它使得当前线程暂时退出同步域,以便其它线程进入。具体请看本文 SynchronizationAttribute 小节。
WaitHandle 包罗有以下几个派生类:
- ManualResetEvent
- AutoResetEvent
- CountdownEvent
- Mutex
- Semaphore
ManualResetEvent
可以壅闭一个或多个线程,直到收到一个信号告诉 ManualResetEvent 不要再壅闭当前的线程。 注意全部期待的线程都会被唤醒。
可以想象 ManualResetEvent 这个对象内部有一个信号状态来控制是否要壅闭当前线程,有信号不壅闭,无信号则壅闭。这个信号我们在初始化的时候可以设置它,如ManualResetEvent event=new ManualResetEvent(false);这就表明默认的属性是要壅闭当前线程。
代码举例:- ManualResetEvent _manualResetEvent = new ManualResetEvent(false);
- private void ThreadMainDo(object sender, RoutedEventArgs e)
- {
- Thread t1 = new Thread(this.Thread1Foo);
- t1.Start(); //启动线程1
- Thread t2 = new Thread(this.Thread2Foo);
- t2.Start(); //启动线程2
- Thread.Sleep(3000); //睡眠当前主线程,即调用ThreadMainDo的线程
- _manualResetEvent.Set(); //有信号
- }
- void Thread1Foo()
- {
- //阻塞线程1
- _manualResetEvent.WaitOne();
-
- MessageBox.Show("t1 end");
- }
- void Thread2Foo()
- {
- //阻塞线程2
- _manualResetEvent.WaitOne();
-
- MessageBox.Show("t2 end");
- }
复制代码 AutoResetEvent
用法上和 ManualResetEvent 差不多,不再赘述,区别在于内在逻辑。
与 ManualResetEvent 差别的是,当某个线程调用Set方法时,只有一个期待的线程会被唤醒,并被允许继续执行。假如有多个线程期待,那么只会随机唤醒此中一个,其它线程仍然处于期待状态。
另一个差别点,也是为什么取名Auto的原因:AutoResetEvent.WaitOne()会主动将信号状态设置为无信号。而一旦ManualResetEvent.Set()触发信号,那么恣意线程再调用 ManualResetEvent.WaitOne() 就不会壅闭,除非在此之前先调用anualResetEvent.Reset()重置为无信号。
CountdownEvent
它的信号有计数状态,可递增AddCount()或递减Signal(),当到达指定值时,将会解除对其期待线程的锁定。
注意:CountdownEvent 是用户模式锁。
Mutex
Mutex 这个对象比较“专制”,同时段内只能答应一个线程工作。
Semaphore
对比 Mutex 同时只有一个线程工作,Semaphore 可指定同时访问某一资源或资源池的最大线程数。
轻量级同步
.NET Framework 4 开始,System.Threading 命名空间中提供了六个新的数据结构,这些数据结构允许细粒度的并发和并行化,并且降低一定必要的开销,它们称为轻量级同步原语,它们都是用户模式锁,包括:
- Barrier
- CountdownEvent(上文已介绍)
- ManualResetEventSlim (ManualResetEvent 的轻量替代,注意,它并不继承 WaitHandle)
- SemaphoreSlim (Semaphore 轻量替代)
- SpinLock (可以以为是 Monitor 的轻量替代)
- SpinWait
Barrier
当在需要一组任务并行地运行连续串的阶段,但是每一个阶段都要期待其他任务完成前一阶段之后才能开始时,您可以通过使用Barrier类的实例来同步这一类协同工作。当然,我们现在也可以使用异步Task方式更直观地完成此类工作。
SpinWait
假如期待某个条件满意需要的时间很短,而且不希望发生昂贵的上下文切换,那么基于自旋的期待时一种很好的替换方案。SpinWait不仅提供了根本自旋功能,而且还提供了SpinWait.SpinUntil方法,使用这个方法能够自旋直到满意某个条件为止。别的 SpinWait 是一个Struct,从内存的角度上说,开销很小。
需要注意的是:长时间的自旋不是很好的做法,由于自旋会壅闭更高级的线程及其相关的任务,还会壅闭垃圾回收机制。SpinWait 并没有筹划为让多个任务或线程并发使用,因此需要的话,每一个任务或线程都应该使用本身的 SpinWait 实例。
当一个线程自旋时,会将一个内核放入到一个繁忙的循环中,而不会让出当前处置处罚器时间片剩余部分,当一个任务大概线程调用Thread.Sleep方法时,底层线程大概会让出当前处置处罚器时间片的剩余部分,这是一个大开销的操作。
因此,在大部分情况下, 不要在循环内调用 Thread.Sleep 方法期待特定的条件满意 。
SpinLock是对 SpinWait 的简单封装。
本文在腾讯开辟者社区同步发布
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。 |