CLR 线程基础
1、什么是进程:
操作系统三个基本的抽象概念:
-
1 文件 : 对 I/O 设备的抽象表示;
-
2 虚拟内存: 对主存 和 磁盘I/O 设备的抽象;
-
3 进程: 对处理器、主存 和 I/O 设备的抽象表示。
进程是操作系统对一个正在运行的程序的一种抽象。在一个系统上可以同时运行多个进程,而每个进程都好像独占地使用硬件。并发的执行多个进程,是处理器在进程间切换实现的。操作系统实现这种交错执行的机制称为上下文切换。
所谓 上下文 ,就是进程运行时所需要的所有状态信息。比如:PC 和 寄存器文件的当前值,以及主存的内容。当一个进程停止,操作系统将当前进程的上下文保存起来,恢复另一个进程的上下文,将控制权交给另一个进程。另一个进程从它上次停止的地方开始。
逻辑控制流:
程序被编译成一条条指令和数据,这一系列的程序计数器(PC)的值的序列称为:逻辑控制流 每个进程执行它的逻辑控制流的一部分,然后被抢占,然后轮到其他进程。
私有地址空间:
每个进程都有自己的私有地址空间,它关联内存中一段内容。
用户模式和内核模式:
内核模式可以执行任意指令,访问任意内存地址。进程从用户模式切换到内核模式唯一的方法是通过诸如中断、故障、或者陷入系统调用这样的异常。
上下文切换的时机:
1 内核代表用户执行系统调用时;
2 sleep 系统调用;
3 中断也可引起上下文切换。中断:系统都有某种产生周期性定时器中断机制,1- 10 毫秒,每次发生中断时内核会判断当前进程运行的时间,是否需要切换到另一个进程。
在从A 进程切换到B 进程之前,内核代表进程A 在用户模式下执行指令。在切换的第一部分,内核代表进程A 在内核模式下执行指令。然后在某一时刻,它开始代表进程B 执行指令(任然是在内核模式下)。在切换之后,内核代表进程B 在用户模式下执行指令。
2、线程的概念
进程是对对处理器、主存 和 I/O 设备的抽象表示,那线程就是对CPU计算资源的抽象表示。Windows 为每个进程都提供了该进程专用的线程(功能相当于一个CPU)。进程中的主线程,是创建进程时为其分配的。进程要做任何事情,都必须让一个线程在它的上下文中运行。该线程负责执行进程地址空间包含的代码。每个线程都有自己的一组 CPU 寄存器和 它自己的堆栈。
线程开销
每个线程都有以下要素:
-
线程内核对象(thread kernel object)
线程内核对象你可以把它想象成是一种数据结构,它保存在OS 为线程分配的内存中。这个数据结构包含:
-
1 描述线程的属性。
-
2 线程上下文(包含CPU 寄存器集合的内存块)。
x86 占700字节,x64占1240字节,ARM 占350 字节。
-
-
线程环境块(thread environment block,TEB)
TEB 是在用户模式(应用程序代码能快速访问的地址空间)中分配和初始化的内存块。TEB 耗用1 个内存页(x86、x64、ARM 都是4KB)。
TEB包含线程的异常处理链首(head)。线程进入的每个try 块都在链首插入一个节点(node);线程退出try块时从链中删除该节点。
此外,TEB还包含线程的“线程本地存储”数据,以及由GDI(Graphics Device Interface,图形设备接口)和 OpenGL 图形使用的一些数据结构。
-
用户模式栈(user-mode stack)
用户模式栈存储传给方法的局部变量和实参。它还包含一个地址:指出当前方法返回时,线程应该从什么地方接着执行。
Windows 默认为每个线程的用户模式栈分配1MB内存。更具体地说,windows 只是保留1 MB地址空间,在线程实际需要时才会提交物理内存。
-
内核模式(kernel-mode stack)
应用程序代码向操作系统中的内核模式函数传递实参时,还会使用内核模式栈。出于对安全的考虑,针对从用户模式的代码传给内核的任何实参,Windows 都会把它们从线程的用户模式栈复制到线程的内核模式栈。32 位windows 内核模式栈 12KB,64位windows是24 KB。
-
DLL 线程连接(attach)和线程分离(detach)通知
windows的一个策略是,任何时候在进程中创建线程,都会调用进程中加载的所有非托管DLL的 DllMain方法,并向该方法传递DLL_THREAD_ATTACH 标志。
类似地,任何时候线程终止,都会调用进程中的所有非托管DLL 的DllMain方法,并向方法传递DLL_THREAD_DETACH 标志。
有的DLL 需要获取这些通知,才能为进程中创建/销毁的每个线程执行特殊的初始化或(资源)清理操作。
我们目前使用的电脑,随便一个进程也得由几百个DLL,要是每次在应用程序中创建一个线程,都必须先调用几百个DLL 函数,然后线程才能开始做它想做的事。在线程终止时还需要将这几百个DLL 函数再调用一遍。这严重影响在进程中创建和销毁线程的性能。
CPU如何上下文切换线程
1 CPU要切换另一个线程,首先要保存当前线程的状态,将要执行的线程数据缓存在高速cache中,高速运行30毫秒后再切换线程. windows大约每30毫秒执行一次上下文切换.
2 正在等待IO操作完成的线程,不会被CPU调度.所以不会浪费CPU时间,
3 一个时间片结束时,如果Windows再次调度同一个线程,那么Windows不会执行上下文切换,接着当前线程继续执行.
尽量避免使用线程,它们消耗大量的内存,需要很多时间来创建,销毁,管理.windows在线程间上下文切换,以及在发生垃圾回收的时候,也会浪费不少时间.
如果只关心性能,任何机器最优的线程数就是它拥有的CPU核数.但是Microsoft设计windows时,追求的是可靠性,和响应能力.
对线程的使用尽量从线程池中获取。在极少数情况可能需要显示创建线程来专门执行一个计算限制的操作。满足以下任何条件,就可以显示创建自己的线程:
-
线程需要以非普通线程优先级运行。所有线程池线程都以普通优先级运行;可以更改线程池的线程优先级,但是不建议那么做。
-
需要线程表现为一个前台线程,防止应用程序在线程结束任务前终止。线程池是始终是后台线程,如果CLR想终止进程,后台线程会跟着终止。
-
计算限制的任务需要长时间运行。线程池为了判断是否需要创建一个额外的线程,所采用的逻辑是比较复杂的。直接为长时间运行的任务创建专用线程,就可以避免这个问题。
-
要启动线程,并可能调用thread 的abort 方法来提前终止它。
要创建专用线程,要构造System.Threading.Thread 类的实例,想构造器传递一个方法名。
using System;
using System.Threading;
public static class Program {
public static void Main() {
Console.WriteLine("Main thread: starting a dedicated thread " +
"to do an asynchronous operation");
Thread dedicatedThread = new Thread(ComputeBoundOp);
dedicatedThread.Start(5);
Console.WriteLine("Main thread: Doing other work here...");
Thread.Sleep(10000); // Simulating other work (10 seconds)
dedicatedThread.Join(); // Wait for thread to terminate
Console.WriteLine("Hit <Enter> to end this program...");
Console.ReadLine();
}
// This method's signature must match the ParameterizedThreadStart delegate
private static void ComputeBoundOp(Object state) {
// This method is executed by a dedicated thread
Console.WriteLine("In ComputeBoundOp: state={0}", state);
Thread.Sleep(1000); // Simulates other work (1 second)
// When this method returns, the dedicated thread dies
}
}
编译并运行上述代码可能得到以下输出:
Main thread: starting a dedicated thread to do an asynchronous operation
Main thread: Doing other work here...
In ComputeBoundOp: state=5
但也可能得到以下输出:
Main thread: starting a dedicated thread to do an asynchronous operation
In ComputeBoundOp: state=5
Main thread: Doing other work here...
线程调度和优先级
抢占式操作系统必须使用算法判断在什么时候调度哪些线程多长时间。前面说过每个线程的内核对象都包含一个上下文结构。上下文结构反映了线程上一次执行完毕后 CPU寄存器的状态。在一个时间片(time-slice)之后,widows检查现存的所有线程内核对象。在这些对象中,只有哪些没有正在等待什么的线程才适合调度。windows 选择一个可调度的线程内核对象,并上下文切换到它。windows还记录了每个线程被上下文切换的次数。
windows 系统做不到很精确的控制线程在你想要的某一时刻执行想要执行的程序。例如:怎样保证一个线程在网络有数据传来的1毫秒内开始运行?这个windows办不到。实时操作系统可以做出这种保证。CLR 使托管代码的行为变得更不“实时”。
windows 支持6 个进程优先级类:
-
Idle、Below Normal、Normal、Above Normal、High 和 Realtime。默认的Normal 是最常用的优先级。
Realtime 优先级要尽可能的避免使用,它的优先级相当高,可以干扰操作系统任务。
windows 支持7 个相对线程优先级:
-
Idle、Lowest、Below Normal、Normal、Above Normal、Highest 和 Time-Critical。
这些优先级是相对于进程优先级类而言的。同样 Normal 是默认优先级。
系统将进程优先级类和其中的一个线程相对优先级映射成一个优先级(0~31)。下表总结了它们的关系。
注意:
-
表中没有值为0 的优先级,因为0优先级保留给零页线程。系统不允许其他线程的优先级为0。
-
而且,以下优先级也不可以获得:17,18,19,20,21,27,28,29 或 30。以内核模式运行的设备驱动程序才能获得这些优先级。
-
Realtime 优先级类中的线程优先级不能低于16。
-
非Realtime 的优先级类中的线程优先级不能高于15。
-
托管应用程序不应该表现为拥有它们自己的进程;相反,它们应该表现为在一个AppDomain 中运行。所以,托管应用程序不应该更改它们的进程的优先级,因为这会影响进程中运行的所有代码。
线程是非常宝贵的资源,必须省着用。使用线程最好的方式就是从线程池中获取。由线程池来管理线程的创建和销毁,这是最可靠的。