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 ...
随机推荐
- HashMap底层分析
以下基于 JDK1.7 分析. 如图所示,HashMap 底层是基于数组和链表实现的.其中有两个重要的参数: 容量 负载因子 容量的默认大小是 16,负载因子是 0.75,当 HashMap 的 si ...
- 转:zabbix 更改maps图标
更改Zabbix map图标 Zabbix的maps用来图形化显示监控设备的拓扑图,并且以不同的标记显示故障事件,通过该图表很直观的显示设备的整体情况.系统默认的图标比较简陋,如图十一所示.通过更改系 ...
- 开始使用Github
Gather ye rosebuds while ye may 我自己也是刚开始使用github没几天,写得不好我就写自己常用的吧 2015年9月20日下午3:19更新知乎上这个答案写得好多了
- 从租人APP沦为性工作发布平台 看共享经济监管边界
看共享经济监管边界" title="从租人APP沦为性工作发布平台 看共享经济监管边界"> 继直播类软件部分涉黄之后,最近火爆的各类"租人"软件 ...
- 6487. 【GDOI2020模拟02.29】列强争霸war
题目描述 区间绝对众数 即出现次数>len/2下取整的数 对于区间[L,R]扫一遍,维护一个数x和出现次数s 当前数=x则s+1,否则s-1,若s已为0则把x设为当前数 若区间内存在绝对众数,那 ...
- 从0开始学Git——Git的常用配置
配置user信息 配置user.name和user.email git config --global user.name 'admin' #设置用户名 git config --global use ...
- C++编码规范(转)
转载链接:https://www.jianshu.com/p/b262d76902e4 一.命名规范 1.通则 1).所有命名都应使用标准的英文单词或缩写,不得使用拼音或拼音缩写,除非该名字描述的是中 ...
- 提高 Web开发性能的 10 个方法
随着网络的高速发展,网络性能的持续提高成为能否在芸芸App中脱颖而出的关键.高度联结的世界意味着用户对网络体验提出了更严苛的要求.假如你的网站不能做到快速响应,又或你的App存在延迟,用户很快就会移情 ...
- 微信小程序学习简介
如何向微信小程序导入DEMO源码: 参考方法 参考学习小程序官方文档 小程序官方文档 小程序目录简介 app.json :设置一些工程全局的量.js : 写一些函数逻辑.wxml: 调用.js中写的函 ...
- canvas初尝试
最近学习了canvas,就拿它做了这么个小东西,感觉已经爱上canvas了.上代码 /* * @auhor : 开发部-前端组-李鑫超 * @property { tableData : {Array ...