ASP.NET CORE中的 多线程 在有限的核心上 运行成千上万的线程(超线程技术)请耐心仔细读取本章 用心体会
原生线程:由操作系统管理的线程
单个逻辑核心在同一时间只能执行一段机器码 也就是一个线程,如果想多个线程同时运行的效果,就要要求线程轮流运行 并且是很短的时间 轮流运行 也就是线程切换
切换线程有两种方式
主动切换 :线程对应的任务 主动要求线程的运行, 比如线程锁被其他线程获取了。读取文件是等待文件响应 主动要求暂停线程
被动切换:线程运行一段时间后,系统强制切换 这种强制切换被称为 抢占.线程在抢占之前 他被运行的最大时间 被称为时间片; 这种抢占机制 基于硬件计时器来实现的;
CPU N个寄存器,各个寄存器的值的数据结构 我们称为 线程之间的切换 ===上下文切换, 每个线程都要关联的寄存器保存切换前后的值

托管线程:.NET管理的线程
.NET 基于原生线程模型====生成.NET管理的线程==托管线程
一个托管线程只能运行一个原生线程
例如 1个Thread对象关联1个原生线程

托管线程对象 会在哪些情况下创建呢?
1托管代码创建 例如Thread.Start方法 创建新的原生线程 并关联
2..NET 运行时内部使用 同时创建原生线程和托管线程 并关联
3 非托管代码:在原生线程上首次调用托管代码的时候
4:.NET程序运行后 主线程去调用Main函数 回创建新的托管对象并关联原生线程
托管线程对象有多种数据结构 :线程本地存储 ,托管函数 非托管函数 分配上下文 执行上下文;由于托管代码需要使用这些数据结构 所以托管代码必须要在托管线程上执行;
如果是非托管代码 ,在原生线程上调用托管代码 他必须要先创建托管线程 并关联后才能调用;
.NET运行时 会把所以托管对象 记录到内部的列表结果;使用该列表结构 可以枚举所有当前的托管线程,去扫描结构中各个托管线程的本地变量 方便GC回收;
.NET运行时 提供了一个标准的线程操作接口 在不同的平台上 去执行 创建 线程,线程锁 等待 ....等等操作;
.NET运行时提供的标准接口实则是封装原生线程对线程的一些列操作
运行时: 管理托管线程对象 (调用系统API操作)关联原生线程
运行时管理托管对象 托管到.NETCore里面 在托管线程中 有两种模式(这两种模式主要是为了GC垃圾回收)
GC,需要找出所有存活的对象 清理没有引用的对象
一个创建或者改变引用 一个清理线程 这就发生了冲突 ,所以说 GC运行的过程中 需要停止其他线程的运行(某个线程获取了某一个.NET运行时内部的线程锁后 GC运行时需要获取同一个锁 这就发生了死锁)
这就引入了 抢占模式 和合作模式
抢占模式:不能访问托管堆上的对象,如果要访问 就得等待GC结束,切换到合作模式;
合作模式: 可以自由访问托管堆上的对象
因为托管代码需要随时访问托管堆 所以托管代码需要处于合作模式下。而非托管代码没有这个限制 ;一旦进入抢占模式 托管代码都会暂停运行,而非托管代码可以运行(前提是此时只能做与.NET无关的操作);
.NET在运行过程中 GC会根据需要切换切换其他线程的模式,所以说在.NET中 所谓的GC停止其他线程 实质上是GC把某个线程切换到 抢占模式
GC切换两种模式 也分为两种模式 主动切换 和被动切换
主动切换:自己切换自己 ,托管代码 同各国P/Invoke调用非托管代码 让自己变成非托管代码 变成抢占模式 当非托管代码执行完毕后 又会被切换为托管代码(切换成合作模式)
主动切换的实现 只需要修改 Thread对象上的一个标记即可 不需要做额外的处理
被动切换:一个线程切换其他线程模式 ,GC线程切换其他线程到抢占模式;
但是从抢占模式切换为合作模式 他必须要保证GC没有运行
合作模式到抢占模式 必须要让GC停在一个GC安全点(GC安全点 <>)
什么是GC安全点呢?===》首先你要知道,JIT在生成汇编代码时会同时生成元数据(其中包括GC信息 GC信息中指示了线程运行到某条指令时,哪些位置有引用类型的对象,而这些对象会被作为根对象进行扫描;)
GC信息不是对每条汇编指令都生成 这样会很浪费资源,会部分选择生成,JIT根据托管函数的大小生成GC信息 如果非常小就会每条都生成 如果比较大 就部分生成; 如果一个托管函数的每一条指令都生成了GC信息 该函数被称为 安全可中断函数 ,反之为部分可中断函数;
函数A---调用函数B---调用函数C 被调用时保证GC都是安全点 实现被动切换
1首先要暂停线程
在Windows、中 暂停线程时调用系统寒素挂起函数 获取当前线程上下文
在Linux/macOS系统中 发送信号到目标线程 去中断线程
2.分析线程是否停在GC安全点
如何分析判断 判断 ===》.NET运行时会从上下文信息中获取程序中值去定位托管函数,再来分析这个托管函数的元素据 去判断是否停在GC安全点 如果没有定位到:就切换不会合作模式 (返回地址劫持技术==线程进行调用跟踪链)
线程的本地化存储
原生实现 :操作系统使用分段寄存器(属于线程上下文切换线程会切换对应的上下文) 来存储指向原生线程的地址
托管实现 .NET运行时使用了原生的本地变量 每个托管对象都关联着一块本地空间 来进行本地存储,托管代码获取托管线程本地变量需要获取托管变量再去获取关联的托管地址;
在.NET中每个托管对象都关联着一个TLB表(Thread Local Block)表 --此表 以AppDomain ID作为索引保存TLM(Thread Local Module)表
听到这你估计是要裂开了 别慌 慢慢来;、
翻译前第二行:TLB表以模块ID为索引 保存托管线程本地存储空间的开始地址 详解看下图:


线程的本地存储 图1 :当我们的变量标记了【ThreadLocal】 这个变量就是线程本地存储变量 (可以看到图二中每个线程本地存储中的变量A与变量B) 图2:程序启动时 fen当执行Thread1 中 a=1时通过原生线程的本地变量获取线程1的线程托管对象 在通过访问TLB表的AppDomainID 获取TLM表其中每个TLM元素就指向变量a 如果你不太清楚为什么要这么存储时 你应该想到那句话;如果一个问题解决不了时,我们应该对他进行包一层的处理,如果在解决不了 那就再包一层
IOC中的 线程单例生命周期 就是利用了线程本地存储实现的



这几个全局变量 管理了所有的线程本地存储的值; LinkedSlotVolatile[] 这个数组用于存储 各个使用了Local的实例 而idManager用于管理数组的索引值 ,ThreadLocal 再创建时 会在idManager中获取一个独立的索引 去访问 LinkedSlotVolatile[]
线程对象结束了肯定要保证本地存储被回收 为了保证能被回收 每一个ThreadLocal都会保存一个双向链表 用于记录所有实际存储过的元素,从继承IDisposable中的销毁的方法 根据链表去重置元素中的值为默认值 并把ID还给idManager (见下图)

总结:
一:线程之前的切换 是为了完成多线程技术
为了线程之间协同合作 ,为了多线程; 一个线程再同一时间只能执行一个线程(机器码 某个任务的机器码);操作系统管理线程 =====》
若要多线程 就需要让多个任务(多个线程)再核心上轮流切换,轮流运行 ;
线程切换有两种方式
1主动切换 :线程的锁被其他线程获取了 自己要求自己停止 ;
2被动切换:线程运行一段时间后超过了一定时间后 系统会强制切换 ===这种强制切换我们称为抢占, 其中线程运行运行的最大时间 我们称为 时间片 抢占机制基于 硬件计时器来实现的;
以上线程切换之后 会在CPU各个寄存器中保存切换线程的上下文 =====》所以 线程的切换 也被称为上下文切换;
二:托管线程
托管线程:基于原生线程创建 创建方法 见上文,=======》托管线程运行后才会与原生线程关联
原生线程:
三:托管线程的管理:GC垃圾回收
当这两个线程同时执行就会发生冲突
所以 GC运行的过程中需要停止其他线程的运行 保证其他线程的引用关系;这种除暴的停止是不安全的,所以 引入了 抢占模式 和 合作模式
托管对象-----切换时需要访问托管堆上的上下文-----
三.1.抢占模式
不能访问托管堆上的对象 等待GC结束 切换为合作模式才能
三.2.合作模式
可以自由访问托管对象
模式的切换分为 主动和被动切换两种
主动切换:自己切换自己 ,托管代码 同各国P/Invoke调用非托管代码 让自己变成非托管代码 变成抢占模式 当非托管代码执行完毕后 又会被切换为托管代码(切换成合作模式)
被动切换:一个线程切换其他线程模式 ,GC线程切换其他线程到抢占模式;

浙公网安备 33010602011771号