C# lock 语法糖实现原理--《.NET Core 底层入门》之自旋锁,互斥锁,混合锁,读写锁
在多线程环境中,多个线程可能会同时访问同一个资源,为了避免访问发生冲突,可以根据访问的复杂程度采取不同的措施
原子操作适用于简单的单个操作,无锁算法适用于相对简单的一连串操作,而线程锁适用于复杂的一连串操作
原子操作
修改状态要么成功且状态改变,要么失败且状态不变,并且外部只能观察到修改前或者修改后的状态,修改中途的状态不能被观察到
.NET 中,System.Threading.Interlocked 类提供了用于执行原子操作的函数,这些函数接收引用参数(ref),也就是变量的内存地址,然后针对该内存地址中的值执行原子操作
无锁算法
不使用线程锁,通过修改操作的内容使它们满足原子操作的条件
.NET 提供了一些线程安全的数据类型,这些数据类型大量应用了无锁算法来提升访问速度(在部分情况下仍需要线程锁):
System.Collections.Consurrent.CurrentBag
System.Collections.Consurrent.CurrentDictionary<TKey, TValue>
System.Collections.Consurrent.CurrentQueue
System.Collections.Consurrent.CurrentStack
线程锁
有获取锁(Acquire)和释放锁(Release)两个操作,在获取锁之后和释放锁之前进行的操作保证在同一个时间只有一个线程执行,操作内容无需改变,所以线程锁具有很强的通用性
线程锁有不同的种类,下面将分别介绍自旋锁,互斥锁,混合锁,读写锁
自旋锁
自旋锁(Spinlock)是最简单的线程锁,基于原子操作实现
它使用一个数值来表示锁是否已经被获取,0表示未被获取,1表示已经获取
获取锁时会先使用原子操作设置数值为1,然后检查修改前的值是否为0,如果为0则代表获取成功,否则继续重试直到成功为止
释放锁时会设置数值为0,其他正在获取锁的线程会在下一次重试时成功获取
使用原子操作的原因是,它可以保证多个线程同时把数值0修改到1时,只有一个线程可以观察到修改前的值为0,其他线程观察到修改前的值为1
.NET 可以使用以下的类实现自旋锁:
System.Threading.Thread.SpinWait
System.Threading.SpinWait
System.Threading.SpinLock
使用自旋锁有个需要注意的问题,自旋锁保护的代码应该在非常短的时间内执行完毕,如果代码长时间运行则其他需要获取锁的线程会不断重试并占用逻辑核心,影响其他线程运行
此外,如果 CPU 只有一个逻辑核心,自旋锁在获取失败时应该立刻调用 Thread.Yield 函数提示操作系统切换到其他线程,因为一个逻辑核心同一时间只能运行一个线程,在切换线程之前其他线程没有机会运行,也就是切换线程之前自旋锁没有机会被释放
互斥锁
由于自旋锁不适用于长时间运行,它的使用场景比较有限,更通用的线程锁是操作系统提供的基于原子操作与线程调度实现的互斥锁(Mutex)
与自旋锁一样,操作系统提供的互斥锁内部有一个数值表示是否已经被获取,不同的是当获取锁失败时,它不会反复重试,而是安排获取锁的线程进入等待状态,并把线程对象添加到锁关联的队列中,另一个线程释放锁时会检查队列中是否有线程对象,如果有则通知操作系统唤醒该线程
因为处于等待状态的线程没有运行,即使长时间不释放也不会消耗 CPU 资源,但让线程进入等待状态与从等待状态唤醒并调度运行可能会花费毫秒级的时间,与自旋锁重试所需的纳秒级时间相比非常的长
.NET 提供了 System.Threading.Mutex 类,这个类包装了操作系统提供的互斥锁,它是可重入的,已经获取锁的线程可以再次执行获取苏锁的操作,但释放锁的操作也要执行相同的次数,可重入的锁又叫递归锁(Recursive Lock)
递归锁内部使用一个计数器记录进入次数,同一个线程每获取一次就加1,释放一次就减1,减1后如果计数器变为0就执行真正的释放操作,一般用在嵌套调用的多个函数中
Mutex 类的另一个特点是支持跨进程使用,创建时通过构造函数的第二个参数可以传入名称
如果一个进程获取了锁,那么在释放该锁前的另一个进程获取同样名称的锁需要等待;如果进程获取了锁,但是在退出之前没有调用释放锁的方法,那么锁会被操作系统自动释放,其他当前正在等待锁(锁被自动释放前进入等待状态)的进程会收到 AbandonedMutexException 异常
跨进程锁通常用于保护多个进程共享的资源或者防止程序多重启动
混合锁
互斥锁 Mutex 使用时必须创建改类型的实例,因为实例包含了非托管的互斥锁对象,开发者必须在不使用锁后尽快调用 Dispose 函数释放非托管资源,并且因为获取锁失败后会立刻安排线程进入等待,总体上性能比较低
.NET 提供了更通用而且更高性能的混合锁(Monitor),任何引用类型的对象都可以作为锁对象,不需要事先创建指定类型的实例,并且涉及的非托管资源由 .NET 运行时自动释放,不需要手动调用释放函数
获取和释放混合锁需要使用 System.Threading.Monitor 类中的函数
C# 提供了 lock 语句来简化通过 Monitor 类获取和释放的代码
混合锁的特征是在获取锁失败后像自旋锁一样重试一定的次数,超过一定次数之后(.NET Core 2.1 是30次)再安排当前进程进入等待状态
混合锁的好处是,如果第一次获取锁失败,但其他线程马上释放了锁,当前线程在下一轮重试可以获取成功,不需要执行毫秒级的线程调度处理;而如果其他线程在短时间内没有释放锁,线程会在超过重试次数之后进入等待状态,以避免消耗 CPU 资源,因此混合锁适用于大部分场景
读写锁
读写锁(ReaderWriterLock)是一个具有特殊用途的线程锁,适用于频繁读取且读取需要一定时间的场景
共享资源的读取操作通常是可以同时执行的,而普通的互斥锁不管是读取还是修改操作都无法同时执行,如果多个线程为了读取操作而获取互斥锁,那么同一时间只有一个线程可以执行读取操作,在频繁读取的场景下会对吞吐量造成影响
读写锁分为读取锁和写入锁,线程可以根据对共享资源的操作类型选择获取读写锁还是写入锁,读取锁可以被多个线程同时获取,写入锁不可以被多个线程同时获取,而且读取锁和写入锁不可以被不同的线程同时获取
.NET 提供的 System.Threading.ReaderWriterLockSlim 类实现了读写锁,
读写锁也是一个混合锁(Hybird Lock),在获取锁时通过自旋重试一定的次数再进入等待状态
此外,它还支持同一个线程先获取读写锁,然后再升级为写入锁,适用于“需要先获取读写锁,然后读取共享数据判断是否需要修改,需要修改时再获取写入锁”的场景
参考资料
《.NET Core 底层入门》
C# lock 语法糖实现原理--《.NET Core 底层入门》之自旋锁,互斥锁,混合锁,读写锁的更多相关文章
- C# lock的语法糖原理--《.net core 底层入门》之自旋锁,互斥锁,混合锁,读写锁
在多线程环境中,多个线程可能会同时访问同一个资源,为了避免访问发生冲突,可以根据访问的复杂程度采取不同的措施 原子操作适用于简单的单个操作,无锁算法适用于相对简单的一连串操作,而线程锁适用于复杂的一连 ...
- 第十三节:实际开发中使用最多的监视锁Monitor、lock语法糖的扩展、混合锁的使用(ManualResetEvent、SemaphoreSlim、ReaderWriterLockSlim)
一. 监视锁(Monitor和lock) 1. Monitor类,限定线程个数的一把锁,两个核心方法: Enter:锁住某个资源. Exit:退出某一个资源. 测试案例:开启5个线程同时对一个变量进行 ...
- 深刨显式锁ReentrantLock原理及其与内置锁的区别,以及读写锁ReentrantReadWriteLock使用场景
13.显示锁 在Java5.0之前,在协调对共享对象的访问时可以使用的机制只有synchronized和volatile.Java5.0增加了一种新的机制:ReentrantLock.与之前提到过的机 ...
- java语法糖---枚举
java语法糖---枚举 在JDK5.0中提供了大量的语法糖,例如:自动装箱拆箱.增强for循环.枚举.泛型等.所谓“语法糖”就是指提供更便利的语法供程序员使用,只是在编译器上做了手脚,却没有提供 ...
- 一文带你.Net混合锁和lock语句
本文主要讲解.Net基于Monitor.Enter和lock实现互斥锁 Monitor.Enter实现 相比前面的锁来说,混合锁的性能更高,任何引用类型的对象都可以做为锁对象,不需要事先创建指定类型的 ...
- Java 线程锁机制 -Synchronized Lock 互斥锁 读写锁
(1)synchronized 是互斥锁: (2)ReentrantLock 顾名思义 :可重入锁 (3)ReadWriteLock :读写锁 读写锁特点: a)多个读者可以同时进行读b)写者必须互斥 ...
- 【原创】读写锁ReentrantReadWriteLock原理分析(一)
Java里面真正意义的锁并不多,其实真正的实现Lock接口的类就三个,ReentrantLock和ReentrantReadWriteLock的两个内部类(ReentrantReadWriteLock ...
- Java语法糖1:可变长度参数以及foreach循环原理
语法糖 接下来几篇文章要开启一个Java语法糖系列,所以首先讲讲什么是语法糖.语法糖是一种几乎每种语言或多或少都提供过的一些方便程序员开发代码的语法,它只是编译器实现的一些小把戏罢了,编译期间以特定的 ...
- lock关键字只不过是C#提供的语法糖
lock关键字只不过是C#提供的语法糖, 最终使用的还是Monitor类. Monitor类的Enter方法要求传入的参数不为null, 否则会有ArgumentNullException excep ...
随机推荐
- 从Surface 3发布 看微软快速转向的根由
3发布 看微软快速转向的根由" title="从Surface 3发布 看微软快速转向的根由"> 平地一声惊雷起,微软突然送惊喜!在毫无征兆的情况下,微软突然发布一 ...
- PHP生成word文档的三种实现方式
PHP生成word原理 利用windows下面的 com组件 利用PHP将内容写入doc文件之中 具体实现: 利用windows下面的 com组件 原理:com作为PHP的一个扩展类,安装过offic ...
- MySQL rand(随机数)、floor(保留整数)、char(ASCII 转字符)、concat(字符串连接)
一.MySQL的rand()函数 select rand(); rand()函数,随机0-1之间的数. 二.获得0-10之间的整数(包含0,不包含10) ; 其中floor()去掉小数. 三.获得指定 ...
- SQL逻辑查询处理顺序特别提醒
我们知道在SQL语句中,第一个被处理的子句式FROM,而不是第一出现的SELECT.这就是SQL不同于其他编程语言的最明显特征之一,以下先看一下SQL查询处理的步骤序号: (8) SELECT (9) ...
- 不同环境下的mvn运行指令
///////////////// 研发环境 //////////////////////////////////////// 清空环境: mvn clean -DAPP_ENV=dev 建立Ecli ...
- nginx 代理第三方邮件站点
需求:公司业务服务器使用的是阿里云,要求内网(仅有内网IP)所有流量走网关服务器(有外网IP及内网IP),内网服务器需要调用一个公网上的第三方邮件站点.在参考了https://www.linuxba. ...
- Springboot整合Dubbo和Zookeeper
Dubbo是一款由阿里巴巴开发的远程服务调用框架(RPC),其可以透明化的调用远程服务,就像调用本地服务一样简单.截至目前,Dubbo发布了基于Spring Boot构建的版本,版本号为0.2.0,这 ...
- rest-framework源码解析和自定义组件----版本
版本 url中通过GET传参自定义的版本 12345678910111213141516171819202122 from django.http import HttpResponsefrom dj ...
- flutter 入门(Mac)
背景 近一年时间都在用 React Native 开发公司的 APP,水平基本上可以说是登堂入室了.前一段时间兴起一阵 Flutter 热,后端的同事都有推荐,今年 Google 大会又推出 flut ...
- AAAI |如何保证人工智能系统的准确性?
|如何保证人工智能系统的准确性?" title="AAAI |如何保证人工智能系统的准确性?"> 注:本文译自AI is getting smarter; ...