.NET中各种线程同步锁
编程编的久了,总会遇到多线程的情况,有些时候我们要几个线程合作完成某些功能,这时候可以定义一个全局对象,各个线程根据这个对象的状态来协同工作,这就是基本的线程同步
。
支持多线程编程的语言一般都内置了一些类型和方法用于创建上述所说的全局对象也就是锁对象
,它们的作用类似,使用场景有所不同。.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 的简单封装。
本文在腾讯开发者社区同步发布
.NET中各种线程同步锁的更多相关文章
- .net中的线程同步基础(搬运自CLR via C#)
线程安全 此类型的所有公共静态(Visual Basic 中为 Shared)成员对多线程操作而言都是安全的.但不保证任何实例成员是线程安全的. 在MSDN上经常会看到这样一句话.表示如果程序中有n个 ...
- Java中的线程同步
Java 中的线程同步问题: 1. 线程同步: 对于访问同一份资源的多个线程之间, 来进行协调的这个东西. 2. 同步方法: 当某个对象调用了同步方法时, 该对象上的其它同步方法必须等待该同步方法执行 ...
- Python之路(第四十四篇)线程同步锁、死锁、递归锁、信号量
在使用多线程的应用下,如何保证线程安全,以及线程之间的同步,或者访问共享变量等问题是十分棘手的问题,也是使用多线程下面临的问题,如果处理不好,会带来较严重的后果,使用python多线程中提供Lock ...
- Java提高班(三)并发中的线程同步与锁
乐观锁.悲观锁.公平锁.自旋锁.偏向锁.轻量级锁.重量级锁.锁膨胀...难理解?不存的!来,话不多说,带你飙车. 上一篇介绍了线程池的使用,在享受线程池带给我们的性能优势之外,似乎也带来了另一个问题: ...
- java中实现线程同步
为何要使用同步? java允许多线程并发控制,当多个线程同时操作一个可共享的资源变量时(如数据的增删改查), 将会导致数据不准确,相互之间产生冲突,因此加入同步锁以避免在该线程没有完成操作之前,被其他 ...
- Windows API学习---用户方式中的线程同步
前言 当所有的线程在互相之间不需要进行通信的情况下就能够顺利地运行时, Micrsoft Windows的运行性能最好.但是,线程很少能够在所有的时间都独立地进行操作.通常情况下,要生成一些线程来处理 ...
- C++11 中的线程、锁和条件变量
转自:http://blog.jobbole.com/44409/ 线程 类std::thread代表一个可执行线程,使用时必须包含头文件<thread>.std::thread可以和普通 ...
- 多线程 - 线程同步锁(lock、Monitor)
1. 前言 多线程编程的时候,我们不光希望两个线程间能够实现逻辑上的先后顺序运行,还希望两个不相关的线程在访问同一个资源的时候,同时只能有一个线程对资源进行操作,否则就会出现无法预知的结果. 比如,有 ...
- java基础---Java---面试题---银行业务调度系统(线程同步锁、枚举、线程池)
银行业务调度系统的项目需求: 模拟实现银行业务调度系统逻辑,具体需求如下: Ø 银行内有6个业务窗口,1- 4号窗口为普通窗口,5号窗口为快速窗口,6号窗口为VIP窗口. Ø 有三种对应类 ...
- Java线程同步锁
把synchronized当作函数修饰符时,示例代码如下: Public synchronized void method(){ //-. } 这也就是同步方法,那这时synchronized锁定的是 ...
随机推荐
- .NET Core MVC基础之返回文件类型
.NET Core MVC基础之返回文件类型 前言 上一篇文章讲了基础的返回类型,这篇文章讲解如何返回文件类型给浏览器下载. 系列文章 .NET MVC基础之页面传值方式 通过图片流来返回图片 返回类 ...
- .NET6 个人博客-推荐文章加载优化
个人博客-推荐文章加载优化 前言 随着博客文章越来越多,那么推荐的文章也是越来越多,之前推荐文章是只推荐8篇,但是我感觉有点少,然后也是决定加一个加载按钮,也是类似与分页的效果,点击按钮可以继续加载8 ...
- Linux Driver : gpio-keys
Linux Driver : gpio-keys的解析 背景 在阅读高通设备树配置一个按键的时候,没有找到按键是在什么时候进行处理的.因此根据仅有的线索gpio-key.c进行分析,发现根据之前的学习 ...
- python3 安装pyodbc失败 pip3 install pyodbc
python3 安装pyodbc失败 报错1: 关键报错信息: fatal error: sql.h: No such file or directory [root@centfos python3 ...
- CosyVoice多语言、音色和情感控制模型,one-shot零样本语音克隆模型本地部署(Win/Mac),通义实验室开源
近日,阿里通义实验室开源了CosyVoice语音模型,它支持自然语音生成,支持多语言.音色和情感控制,在多语言语音生成.零样本语音生成.跨语言声音合成和指令执行能力方面表现卓越. CosyVoice采 ...
- Java异步判断线程池所有任务是否执行完成的方法
1.使用ExecutorService和CountDownLatch的方法示例 在Java中,当我们使用线程池(如ExecutorService)来执行异步任务时,常常需要知道所有任务是否都已经完成. ...
- webgl智慧楼宇发光系列之线性采样下高斯模糊
目录 webgl智慧楼宇发光系列之线性采样下高斯模糊 效率问题 线性采样 代码讲解 总结 参考文档 webgl智慧楼宇发光系列之线性采样下高斯模糊 前面一篇文章 <webgl智慧楼宇发光效果算法 ...
- 写了一个json小工具,希望大家体验(Mac平台)
用rust写了一个json小工具"JSON PICKER",欢迎大家试用: https://github.com/davelet/json-picker/releases/tag/ ...
- [oeasy]python0140_导入_import_from_as_namespace_
导入import 回忆上次内容 上次学习了 try except 注意要点 半角冒号 缩进 输出错误信息 有错就报告 不要隐瞒 否则找不到出错位置 还可以用traceback把 系统报错信息原 ...
- [MAUI 项目实战] 笔记App:程序设计
前言 有人说现在记事类app这么多,市场这么卷,为什么还想做一个笔记类App? 一来,去年小孩刚出生,需要一个可以记录喂奶时间的app,发现市面上没有一款app能够在两步内简单记录一个时间,可能iOS ...