进程与线程
进程(Process)是引用程序的实例要使用的资源的一个集合(进程就是一种资源,是应用程序所用的资源,一个exe就是一个进程),每个进程都被赋予了一个虚拟地址空间,每个应用程序都在各自的进程中运行来确保应用程序不受其他应用程序的影响,进程是操作系统为我们提供的一种保护应用程序的一种机制。
线程(Thread)的职责是对CPU进行虚拟化,windows为每个进程都提供了该进程专业的线程(功能相当于一个CPU,可将线程理解成一个逻辑CPU),线程是进程中基本执行单元, 一个进程中可以包含多个线程,在进程入口执行的第一个线程是一个进程的主线程,在.Net应用程序中,都是以Main()方法作为程序的入口的, 所以在程序运行过程中调用这个方法时,系统就会自动创建一个主线程。(他们之间的关系简单说:线程是进程的执行单元,进程是线程的一个容器了)。
前台线程和后台线程
CLR将每个线程要么视为前台线程,要么视为后台线程,一个进程中所以前台线程停止运行时,CLR强制终止仍在运行的后台线程,这些后台线程被直接终止,不好抛出异常。因此,前台线程应该用于执行确实想完成的任务,比如讲数据库内存缓冲器flush到磁盘,另外,应该为非关键的任务使用后台线程,比如 重新计算电子表格的单元格,或者为记录建立索引。这是由于这些工作能在应用程序重启时继续,而且如果用户想终止应用程序,就没有必要强波她保存活动状态。下面通过代码我看看前台线程与后台线程的区别:
class Program { static void Main(string[] args) { // 创建一个新线程(默认为前台线程) Thread backthread = new Thread(Worker); // 使线程成为一个后台线程 backthread.IsBackground = true; // 通过Start方法启动线程 backthread.Start(); // 如果backthread是前台线程,则应用程序大约5秒后才终止 // 如果backthread是后台线程,则应用程序立即终止 Console.WriteLine("Return from Main Thread"); } private static void Worker() { // 模拟做10秒 Thread.Sleep(10000); // 下面语句,只有由一个前台线程执行时,才会显示出来 Console.WriteLine("Return from Worker Thread"); } }
运行上面代码可以发现:控制台中显示字符串: Return form Main Thread 后就退出了, 字符串 Return from Worker Thread字符串根本就没有显示,这是因为此时的backthread线程为后台线程,当主线程(执行Main方法的线程,主线程当然也是前台线程了)结束运行后,CLR会强制终止后台线程的运行,整个进程就被销毁了,并不会等待后台线程运行完后才销毁。如果把 backthread.IsBackground = true; 注释掉后, 就可以看到控制台过5秒后就输出 Return from Worker Thread。再在Worker方法最后加一句 代码:Console.Read(); 就可以看到这样的结果了
线程开销
线程不是没有消耗的,相反每开一个线程它的开销也是昂贵的,线程会产生空间(内存消耗)和时间(运行时的执行性能)上的开销,下面我们一起来看看每个线程中有哪些开销。
- 线程内核对象(thread kernel object)OS(操作系统)为系统中创建的每个线程都分配并初始化这种数据结构之一,在改数据结构中,包含一组对线程进行描述的属性,数据结构中还包含所谓的线程上下文(thread context),上下文是一个内存块,其中包含了CPU的寄存器集合,windows在一台X86CPU的计算机运行时,线程上下文使用约700字节的内存,对象X64和IA64CPU,上下文分布使用约1240字节和2500字节的内存
- 线程环境快(thread environment block,TEB)TEB是在用户模式(应用程序代码能快速访问的地址空间)中分配和初始化的一个内存卡,TEB耗用1个内存页(X86和X64CPU中是4k,IA64CPU中是8kb)。TEB包含线程的异常处理链首,线程进入的每个try快都在链首插入一个字节,线程退出try快时,会从链中删除该节点,除此之外,TEB还包含线程的 “线程本地存储”数据,以及有GDI喝OpenGL图形使用到一些数据结构。
- 用户模式栈(user-mode stack)用户模式栈用于存储给方法的局部变量和实参,它还包含一个地址;指出当前方法返回时,线程接着应该从什么地方开始,默认情况下,windows为每个线程的用户模式栈分配1MB内存。
- 内核模式栈(kernel-mode stack) 应用程序代码向操作系统中的一个内核模式的函数传递实参时,还会使用内核模式栈,出于安全方面的原因,针对用户模式的代码传给内核的任何实参,windows都会把它们从线程的用户模式栈复制到线程的内核模式栈。一经复制,内核就可验证实参的值,由于应用程序代码不能访问内核模式栈,所以应用程序无法修改验证之后的实参值,OS内核代码将开始对复制的值进行处理,除此之外,内核会调用它自己的内部方法,并利用内核模式栈传递它自动的实参、存储函数的局部变量以及存储返回地址。在32位windows上运行时,内核模式栈大小为12kb;在64为windows上运行时,大小为24kb.
- DLL线程连接(attach)和线程分离(detach)通知 windows的一个策略是,任何时候在进程创建一个线程,都会调用那个进程中加载的索引DLL的DLLMain方法,并向该方法传递一个DLL_THREAD_ATTACH标志。类似的,任何时候一个线程终止,都会调用进程中所以DLL的DLLMain方法,并向该方法传递一个Dll_THREAD_DETACH标志,有的dll需要利用这些通知,为进程中创建/销毁的每个线程执行一些特殊的初始化清理操作,例如 C—Runtime库DLL会分配一些线程本地存储状态,线程只要C-Runtime库中包含的函数时,需要这些状态
- 上下文切换 对单CPU计算机来说,操作系统每次只将一个线程分配给CPU执行,执行完后将线程上下文数据记录下来保存在线程内核对象结构中;然后装载另一个线程的上下文,将CPU执行控制交给此线程,如果该线程有另一个进程拥有,那么在装载该线程之前,Windows还必须使得CPU能够处理该虚拟地址空间。Windows操作系统为各个线程每次分配大概30毫秒的执行时间,称为“时间片”。上下文切换是净开销,不会换来任何在存储空间或者性能上的收益。但是能向用户提供一个健壮的能灵活相应的操作系统。
因此我们应尽可能地避免 使用线程,因为它们要耗用大量的内存,而且需要相当多的时间来创建、销毁和管理。windows在线程直接进行上下文切换,以及在发生垃圾回收的时候,也会浪费不少时间,但是有时候我们又必须使用线程,因为它们时windows变得更健壮,反应更快,
看了创建线程时消耗,让我一个线程小白也懂的了,线程虽好,小友可不要贪杯哦!当然微软也为我们提供了解决方案那就是线程池大哥了,关于线程池的使用我们后面在介绍, 但是线程还是有线程的优势,我们在选择何时何地使用线程就变得更重要了
在选择使用线程的时候需要知道我们的任务是 计算密集型还是IO密集型
计算密集型任务的特点是要进行大量的计算,消耗CPU资源,比如计算圆周率、对视频进行高清解码等等,全靠CPU的运算能力。这种计算密集型任务虽然也可以用多任务完成,但是任务越多,花在任务切换的时间就越多,CPU执行任务的效率就越低,所以,要最高效地利用CPU,计算密集型任务同时进行的数量应当等于CPU的核心数。
IO密集型,涉及到网络、磁盘IO的任务都是IO密集型任务,这类任务的特点是CPU消耗很少,任务的大部分时间都在等待IO操作完成(因为IO的速度远远低于CPU和内存的速度)。对于IO密集型任务,任务越多,CPU效率越高,但也有一个限度。常见的大部分任务都是IO密集型任务,比如Web应用。
我的理解是对于计算密集型的任务,因为是消耗CPU资源,我个人觉得应该创建线程去后台执行,要充分利用多个CPU的资源
而对于IO密集型的任务应该使用异步执行