.NET Core多线程 (4) 锁机制
合集:.NET Core多线程温故知新
- .NET Core多线程(1)Thread与Task
- .NET Core多线程(2)异步 - 上
- .NET Core多线程(3)异步 - 下
- .NET Core多线程(4)锁机制
- .NET Core多线程(5)常见性能问题
去年换工作时系统复习了一下.NET Core多线程相关专题,学习了一线码农老哥的《.NET 5多线程编程实战》课程,我将复习的知识进行了总结形成本专题。
本篇,我们来复习一下.NET中锁机制的相关知识点,预计阅读时间10分钟。
理解lock锁的底层原理
(1)为什么要用锁?
对某个共享代码区域(临界区)进行串行访问,使用lock来保证串行的安全。
(2)lock的用法
lock (lockMe)
{
dict.Add(i.ToString(), DateTime.Now);
}
(3)lock的本质
通过ILSpy反编译查看可以知道,lock是个语法糖,编译后其实是Monitor.Enter 和 Monitor.Exit 的封装。
try
{
Monitor.Enter(lockMe, ref lockTake); dict.Add(i.ToString(), DateTime.Now);
}
finally
{
if (lockTake)
{
Monitor.Exit(lockMe);
}
}
(4)lock为何需要引用类型?
首先,编译器要求lock中的所对象必须是引用类型。
其次,因为lock会用到对象头中的同步块索引来进行同步,值类型没有堆中的数据。

无锁化:线程的本地存储
(1)线程本地存储
static 的作用域在AppDomain下都可见,此时在多线程环境中,通过static共享变量的方式来同步,不可避免会出现锁竞争。如果能将作用域范围缩小,比如缩小到Thread级别,就可以避免锁竞争。例如:ConcurrentBag就是一个好的例子。
(2).NET中的解决方案
ThreadStatic(Attribute):当前线程拿到的是定义好的值,其他线程拿到的可能是默认值(值类型可能是0,引用类型可能是null,需要注意容错)。
ThreadLocal:与ThreadStatic最大的区别在于ThreadStatic只在第一个线程初始化,ThreadLocal则会为每个线程初始化。
(3)存储在哪里?
- PEB 进程环境块
- TEB 线程环境块
- TLS 线程本地存储(Thread Local Storage),取决于一共有多少个DataSlot
(4)应用场景
用来做数据库连接池:DB连接池 基于 ThreadLocal实现,每个线程只能看见自己的请求队列;
用来做链式追踪:比如Skywalking或Zipkin等,用到ThreadLocal做本地存储,记录完整的调用链条如:A -> B -> C -> D;
内核态锁知多少
(1)基于WaitHandle的内核锁
这种锁是基于Windows底层的内核数据结构来维护线程之间的同步,比如:
AutoResetEvent / ManualResetEvent
Semaphore
Mutex
(2)优缺点
需要从用户态切换到内核态,相对来说比较重量级,相对耗费时间;内核模式的锁,不仅可用于创建线程同步,还可以创建进程同步。
用户态锁知多少
(1)用户态锁是啥?
例如下面的代码:
lock(obj)
{
... // todo [1ms]
}
大部分都是在临界区进行等待时间很短(比如1ms)的加锁,能不能让thread在CLR或C#层面内旋(自旋)一下,从而提高性能呢?使用用户态锁就可以避免上下文切换和内核切换带来的高开销。
(2)寻找解决方案
保持线程在用户态又要尽可能少的消耗CPU时间
时间片
- Windows中一个时间片大概是30ms
- Thread.Sleep(0)
- 提前结束自己的时间片,然后把自己放入到就绪队列中,如果就绪队列中的线程优先级 >= Current Thread,那么其他线程会被调度
- 如果就绪队列中的线程优先级 < Current Thread,那么Current Thread只能继续执行【低优先级线程得不到执行】
- 整体CPU级别
- Thread.Yield()
- 提前结束自己的时间片,如果当前逻辑CPU上的就绪队列上有待执行的线程,那么这个线程就会被调度(不考虑优先级)【低优先级线程可以得到执行】
- 逻辑CPU级别
极端休眠时间
- Sleep(1)
- 本质上和Sleep(1000)一样,都需要休眠
CAS原语
- read, operate, write => 打包成原子性
借助CLR内的AwareLock::SpinWait()
- C# SpinWait
- CLR SpinWait
(3).NET内置的SpinLock(用户态)
SpinLock在用法上和lock关键字差不多的。
class Program
{
public static SpinLock spinLock = new SpinLock(); public static int counter = 0; static void Main(string[] args)
{
Parallel.For(1, 1000001, (i) =>
{
var lockTaken = false;
spinLock.Enter(ref lockTaken);
++counter;
spinLock.Exit();
}
}); Console.WriteLine($"counter={counter}"); Console.ReadLine();
}
(4).NET CAS案例:Interlocked
CPU直接操作的,主要用在一些简单类型上:
read
operation
write
class Program
{
public static SpinLock spinLock = new SpinLock(); public static int counter = 0; static void Main(string[] args)
{
Parallel.For(1, 1000001, (i) =>
{
Interlocked.Increment(ref counter, 1);
}); Console.WriteLine($"counter={counter}"); Console.ReadLine();
}
混合态锁知多少
混合锁:用户态模式+内核态模式
(1)ManualResetEventSlim
它是如何实现的?
- lock
- ManualResetEvent
- CAS
- SpinWait(轻量级自旋锁)、SpinLock
(2)SemaphoreSlim
它是如何实现的?
- ManualResetEvent + lock + SpinWait
(3)ReaderWriterLockSlim
这个锁的内核版是 ReaderWriterLock,不带Slim就代表是内核态的锁。
这个锁顾名思义是读写锁,意思是:读可以并行,但写只能串行。EnterWriteLock() 需要等待所有的reader或writer锁结束,才能开始
(4)CountdownEvent
这个锁可以实现类似MapReduce的效果。
它是如何实现的?
基于ManualResetEvent事件做了底层封装。
线程安全集合知多少
(1)线程安全集合
.NET中都有哪些线程安全的集合类型?
ConcurrentBag 对应非线程安全类型:List
ConcurrentQueue 对应非线程安全类型:Queue
ConcurrentStack 对应非线程安全类型:Stack
ConcurrentDictionary 对应非线程安全类型:Dictionary
(2)BlockingCollection
BlockingCollection 意为 阻塞集合。
线程安全的集合 可以转换为 阻塞集合,只要它实现了IProducerConsumerCollection接口BlockingCollection可以实现类似发布订阅的业务场景应用:
生产端Add进去发布的消息
消费者端通过GetConsumingEnumerable()方法阻塞等待发布的消息
ConcurrentDictonary的两个大坑
(1)Values的坑
观察现象
业务场景:自己用ConcurrentDictionary封装了一个Cache
FullGC 将 LOH 上的对象回收了
所有>=85000byte的都会被纳入LOH
观察源码
Values方法每次都会生成一个新的List集合对象进行返回,每个对象都是大对象
如何改进
禁止调用Values方法
借助lock + Dictionary实现类似操作避免每次生成新的List集合对象
(2)GetOrAdd的坑
观察现象
业务场景:自己用ConcurrentDictionary封装了一个Redis连接池缓存
借助GetOrAdd实现的CreateInstance方法未能实现线程安全导致连接池被大量反复创建
观察源码
GetOrAdd方法中的valueFactory不是线程安全的
如何改进
借助Lazy改造字典的Value对象,保证创建方法只被执行一次,比如:将RedisConnection改为Lazy
共享变量在Release模式下的Bug
(1)现象
同样的代码,通过共享变量控制工作线程是否要结束自己,在Debug模式下没有问题,但是在Release模式下有问题。
(2)原因
JIT提供了错误的决策导致CPU在解析代码时做了优化,将 共享变量 存放在了CPU的寄存器中。
(3)WinDbg探究
Release模式
查看memory中的共享变量的值
CPU寄存器
查看共享变量的值
(4)解决方案
使用CancellationToken做取消
不用Cache,都读内存address中的对象,性能会相对较低
将共享变量 改为 易变结构,比如:private bool _shouldStop 改为 private volatile bool _shouldStop
小结
本篇,我们复习了锁机制相关的知识点。下一篇,我们将复习一下常见的.NET多线程相关的性能优化实践。
参考资料
一线码农,腾讯课堂《.NET 5多线程编程实战》
不明作者,《Task调度与await》

.NET Core多线程 (4) 锁机制的更多相关文章
- SDL 开发实战(七): SDL 多线程与锁机制
为什么要用多线程?在音视频领域主要是实现音视频同步.实现了音视频同步,我们的播放器就基本上合格了. 这里我们将讲解一下SDL的多线程与锁机制. 多线程的好处主要是能使程序更加充分利用硬件(主要是CPU ...
- C 语言多线程与锁机制
C 语言多线程与锁机制 多线程 #include <pthread.h> void *TrainModelThread(void *id) { ... pthread_exit(NULL) ...
- JAVA多线程与锁机制
JAVA多线程与锁机制 1 关于Synchronized和lock synchronized是Java的关键字,当它用来修饰一个方法或者一个代码块的时候,能够保证在同一时刻最多只有一个线程执行该段代码 ...
- Java多线程(二) 多线程的锁机制
当两条线程同时访问一个类的时候,可能会带来一些问题.并发线程重入可能会带来内存泄漏.程序不可控等等.不管是线程间的通讯还是线程共享数据都需要使用Java的锁机制控制并发代码产生的问题.本篇总结主要著名 ...
- java多线程(三)——锁机制synchronized(同步语句块)
用关键字synchronized声明方法在某些情况下是有弊端的,比如A线程调用同步方法之行一个长时间的任务,那么B线程必须等待比较长的时间,在这样的情况下可以使用synchronized同步语句快来解 ...
- java多线程(二)——锁机制synchronized(同步方法)
synchronized Java语言的关键字,可用来给对象和方法或者代码块加锁,当它锁定一个方法或者一个代码块的时候,同一时刻最多只有一个线程执行这段代码.当两个并发线程访问同一个对象object中 ...
- java 多线程总结篇4——锁机制
在开发Java多线程应用程序中,各个线程之间由于要共享资源,必须用到锁机制.Java提供了多种多线程锁机制的实现方式,常见的有synchronized.ReentrantLock.Semaphore. ...
- Lucene的多线程访问原则和同步,锁机制
本文介绍lucene多线程环境下的使用原则和commit.lock与write.lock实现的锁机制. 设计之初就是服务于多线程环境,大多数情况下索引会被不至一个线程访问.索引时一个关键资源.在对这样 ...
- [java多线程] - 锁机制&同步代码块&信号量
在美眉图片下载demo中,我们可以看到多个线程在公用一些变量,这个时候难免会发生冲突.冲突并不可怕,可怕的是当多线程的情况下,你没法控制冲突.按照我的理解在java中实现同步的方式分为三种,分别是:同 ...
- Java CAS同步机制 原理详解(为什么并发环境下的COUNT自增操作不安全): Atomic原子类底层用的不是传统意义的锁机制,而是无锁化的CAS机制,通过CAS机制保证多线程修改一个数值的安全性。
精彩理解: https://www.jianshu.com/p/21be831e851e ; https://blog.csdn.net/heyutao007/article/details/19 ...
随机推荐
- 2021-06-28:最接近目标值的子序列和。给你一个整数数组 nums 和一个目标值 goal 。你需要从 nums 中选出一个子序列,使子序列元素总和最接近 goal 。也就是说,如果子序列元素和
2021-06-28:最接近目标值的子序列和.给你一个整数数组 nums 和一个目标值 goal .你需要从 nums 中选出一个子序列,使子序列元素总和最接近 goal .也就是说,如果子序列元素和 ...
- 2021-12-02:给定一个字符串str,和一个正数k。 返回长度为k的所有子序列中,字典序最大的子序列。 来自腾讯。
2021-12-02:给定一个字符串str,和一个正数k. 返回长度为k的所有子序列中,字典序最大的子序列. 来自腾讯. 答案2021-12-02: 单调栈.先进来的元素大,后进来的元素小. 时间复杂 ...
- Go开源世界主流成熟ORM框架gorm实践分享
@ 目录 概述 定义 核心功能 声明模型与约定 gorm.Model 字段级权限 时间惯例 嵌入结构 字段标签 使用 安装 数据库链接 连接池 CRUD 接口 创建 查询 高级查询 修改 删除 原始S ...
- 学习原根 by OI-wiki
根据 OI-wiki 的讲解,加以自己的理解和简化.偏重于算法竞赛而不是数学竞赛. 前置知识: 费马小定理:\(a^{p-1} \equiv 1(\mod p)\),\(p\) 为质数. 欧拉定理:\ ...
- ChatGPT 问答
Win32GUI编程时,创建窗口触发消息的顺序 在Win32 GUI编程中,创建窗口并显示到屏幕上时,系统会触发一系列的消息,这些消息可以用来完成窗口的初始化和其他相关的工作.下面是创建窗口触发消息的 ...
- AI在金融体系下如何引导API安全?
AI在金融体系下如何引导API安全? 随着金融科技的快速发展,越来越多的金融机构开始使用API(应用程序接口)进行数字化转型,加速业务流程和数据交换.然而,API在金融体系下面临诸多安全威胁,如恶意攻 ...
- WPF中实现含有中心点Slider双向滑动条
想要实现的效果 原生滑动条 需要认识一下滑动条的组成 在原生控件中生成"资源字典"对应的样式 然后在track所在的列进行添砖加瓦 由于track在row="1" ...
- Vue跨域配置异常采坑:Request failed with status code 401
本地用Express作为服务端,前端Vue项目配置跨域代理,调用服务端api接口始终报错"Request failed with status code 401".原来发现是端口3 ...
- 云原生时代崛起的编程语言Go远程调用gRPC实战
@ 目录 概述 定义 背景 特点 四种服务方法 实战 环境配置 proto文件 简单RPC Token认证 服务器流式RPC 客户端流式RPC 双向流式RPC 概述 定义 gRPC 官网地址 http ...
- MVCC并发版本控制之重点ReadView
MVCC并发版本控制 本文大部分来自<MySQL是怎样运行的>,这里只是简单总结,用于各位回忆和复习. 版本链 对于使用 InnoDB 存储引擎的表来说,它的聚簇索引记录中都包含两个必要的 ...