CPU核心数量拥有成千上万的线程,一个线程负责执行一个任务,可分为原生线程和托管线程;【寄存器 < 逻辑核心 < CPU】
原生线程:
原生线程切换我们将操作系统管理的线程称为原生线程,CPU中的逻辑核心都有N个寄存器,核心根据寄存器从内存中读取指令并运行一段机器码(某逻辑的处理内容),所以,单个逻辑核心同一时间只能执行一个线程(线程切换执行),线程切换可分为主动切换,比如线程内任务完成或IO读取文件等行为主动暂停运行,以及被动切换,即硬件计时器时间片时间超时后,将被动切换;
CPU中各个寄存器的数据结构则成为上下文,而线程切换称为上下文切换;上下文切换的成本主要在于上下文数据的保存;
托管线程
.Net Core基于原生线程搭建的线程模型,由.Net Core管理的线程;
一个托管线程只能管理一个原生线程,可以为 0:1 (未开始、未关联线程的线程)或 1:1的关系,一个Thread对象就关联一个原生线程;
==> 1,托管代码创建中,只有调用Start方法后,才会创建新的原生线程并关联线程对象;
===>2,.Net运行时内部使用时,将同时创建;
===>3,非托管代码在原生线程上首次调用托管代码时将创建和关联
===>4,Main函数运行.Net提供了标准的线程操作接口,
托管线程
GC 找出存货的对象,清理没有引用的对象,负责执行 ;
扫描和清理对象的GC线程(需要停止其他线程运行,实际为GC切换合作模式到抢占模式);
负责分配对象或改变对象引用关系的其他线程;为了处理上述的矛盾关系:
==>抢占模式,不能访问托管堆上的对象,需要等待GC结束,切换到合作模式
==>合作模式,可自由访问托管堆上的对象,所以,托管代码需要在合作模式下运行托管代码切换:
===>主动切换,线程自己切换,托管代码通过P/Invoke嗲用非托管代码
===>被动切换,一个线程切换其他线程的模式,GC线程切换其他线程到抢占模式线程切换时,GC应该处于安全点,JIT编译器在生成托管汇编代码时会生成元数据,元数据中包含GC信息,该信息包含了线程运行到某条指令时,哪个位置有应用类型的对象,这些对象将会被作为根对象扫描;
步骤:暂停线程 => 分析线程是否处于GC安全点
前台线程和后台线程
Thread创建的都是前台线程,当后台线程全部完成后,前台线程才会结束
线程本地存储
线程本地存储1 线程本地存储2线程内部的相关数据结构,使用特性[ThreadStatic],保持线程间的数据独立性;
通过分段寄存器(上下文一部分)访问到独立的线程存储;
TLB表(Thread Local Block),使用AppDomain ID作为索引保存 TLM表(Thread Local Module)=> 以模块ID为索引记录托管线程本地存储空间的开始地址
原子操作
多线程存在同时访问一个资源,这时,需要一种不可分割且其他原子操作互斥的操作;
.Net 中 Interlocked类提供了对原子操作的相关方法,结合线程锁(适用于复杂的逻辑操作)/无锁算法就能实现资源同一时间只被一个线程使用;【无锁算法通过修改算法结构来抛弃使用线程锁而达到原子操作,如.Net中的Concurrent...开头的线程安全的数据结构】
锁:
==>自旋锁,基于原子操作,0表示未获取,1已被获取,循环检查锁是否被释放,Thread.SpinWait()可实现一个自旋锁;自旋锁应该在较短的时间内完成任务,否则将长期占用CPU运算核心,而且会造成权重高的线程可能得不到核心计算;所以,处理这种情况可以使用排队自旋锁;自旋锁拥有高性能的处理能力;这里也可以使用new SpinWait().SpinOnce()和SpinLock;.Net性能的提高其实很多的地方都改用了这些自旋锁,不适用于长时间运行的操作;
==>互斥锁,获取锁失败时,不重试,而是进入等待,放入等待队列;在线程状态切换(从等待-唤醒-运行)对比自旋锁是很消耗时间的;
===>信号量,Semaphore类,可跨进程使用(SemaphoreSlim类非跨进程);使用线程管理的方式,对多个线程进行锁的应用操作;
===>读写锁,频繁读取并需要一定时间的场景,分为读取锁、写入锁,实质相当于一个混合锁,在自旋次数后,进入线程等待队列中等待,保证 读取锁、写入锁会同时存在同一线程
Thread.Sleep()、Thread.Yield 在Windows下
Sleep()方法:调用系统SleepEx,切换到任意核心关联的带运行队列中的线程,如果参数设置为0;
Yield():当前逻辑核心关联的待运行队列中的线程;
如果是Linux系统,则两者没有区别
网友评论