多线程在项目开发过程中非常非常重要,这个系列就来详细总结一下,首先认识一下多线程。
windows为什么要支持多线程
计算机的早期时代,操作系统没有线程的概念,整个系统只运行着一个执行线程,其中包含操作系统代码和应用程序代码。只用一个执行线程的问题在于,长时间运行的任务会阻止其他任务的执行。例如16位Windows的时代,打印文档的应用程序很容易“冻结”整个机器。
Microsoft 在设计Windows NT这个版本的OS内核时,决定在一个进程中运行应用程序的每个实例。进程实际是应用程序的实例要使用的资源的集合。每个进程都被赋予了一个虚拟地址空间,确保一个进程中使用的代码和数据无法由另一个进程访问。这就确保了应用程序实例的健壮性。同时,进程访问不了OS的内核代码和数据;所以,应用程序代码破坏不了操作系统的代码和数据
如果应用程序发生死循环会发生什么?如果机器只有一个CPU,它会执行死循环,不能执行其他任何程序。Microsoft 的解决方案就是线程。作为一个Windows概念,线程的职责是对CPU进行虚拟化。Windows为每个进程都提供了该进程专用的线程(功能相当于一个CPU)。应用程序的代码进行死循环,与代码关联的进程会“冻结”,但其他进程(它们有自己的线程)不会冻结,它们会继续执行。
线程很强大,因为它们使Windows即使在执行长时间运行的任务时,也能随时响应。
所以多线程的发展历史可以简单总结为:没有线程(只有一个执行线程)--->引入进程--->引入多线程
线程的开销
线程是给我们带来好处的同时,也有性能的损失,包括空间上和时间上的。
1,空间上
创建一个线程需要加载以下资源:
- 线程内核对象(thread kernel object),操作系统为系统中创建的每个线程都会分配并初始化这种数据结构,主要用于描述线程的属性和线程上下文,上下文是一个内存块,其中包含了CPU的寄存器集合。对于X86,X64和IA64的CPU来说,分别要使用700,1240和2500字节的内存。
- 线程环境块(thread environment block,简称TEB),TEB是在用户模式(应用程序能快速访问的内存地址)中分配和初始化的一个内存块,TEB耗用1个内存页(X86和X64 CPU中是4KB,IA64 CPU是8KB)。
- 用户模式栈(user-mode stack),用户模式栈用于存储传给方法的局部变量和实参,它还包含一个地址,指出当前方法返回时,线程接着应该从什么地方执行,默认情况下,windows为每个线程的用户模式栈分配1MB内存。
- 内核模式栈(kernel-mode stack),当应用程序代码向操作系统中的一个内核模式的函数传递实参时,就会使用到内核模式栈。出于安全的考虑,Windowd会把这些实参从线程的用户模式栈复制到线程的内核模式栈。32windows 内核模式栈大小12KB,64位是24KB。
- DLL线程连接(attach)和线程分离(detach)通知,Windows的一个策略是,任何时候在进程中创建线程,都会调用进程中加载的所有非托管DLL的DllMain方法,并向该方法传递DLL_THREAD_ATTACH标志。同样的,任何时候线程终止,都会调用进程中的所有非托管DLL的DllMain方法,并向该方法传递DLL_THREAD_DETACH标志。
2,时间上
因为windows要在系统中的所有线程(逻辑CPU)之间共享物理CPU。在任何给定的时刻,windows只将一个线程分配给一个CPU,那个线程能运行一个“时间片”的长度。时间片到期,Windows就将上下文切换到另一个线程。
每个时间片的切换,windows都需要大概30ms的时间。
为什么要使用多线程
1,可响应性,或称用户体验,一般针对winform程序,可以将一些耗时的任务交给另一个线程去处理,使GUI线程能灵敏地响应用户的输入和操作。否则,界面会比较卡。
2,提升性能,由于windows每个CPU调度一个线程,多个CPU能并行调度线程,所以可以同时执行多个任务,从而提升性能。
进程,线程和应用程序域的关系
在进一步学习多线程之前,很有必要来了解一下这三个概念,以及其中的关系。
1,名词解释
进程
或称Process,可以简单理解为一个.exe的实例。进程是windows系统中的一个基本概念,它包含着一个运行程序所需要的资源。进程之间是相对独立的,一个进程无法访问另一个进程的数据(除非利用分布式计算方式),一个进程运行的失败也不会影响其他进程的运行,Windows系统就是利用进程把工作划分为多个独立的区域的。进程可以理解为一个程序的基本边界。
线程
或称Thread,可以简单理解为虚拟CPU。线程是进程的基本执行单元,在进程入口执行的第一个线程被视为这个进程的主线程。在.NET应用程序中,都是以Main()方法作为入口的,当调用此方法时系统就会自动创建一个主线程。线程主要是由CPU寄存器、调用栈和线程本地存储器(Thread Local Storage,TLS)组成的。CPU寄存器主要记录当前所执行线程的状态,调用栈主要用于维护线程所调用到的内存与数据,TLS主要用于存放线程的状态信息。
应用程序域
或称AppDomain,可以简单理解为一组程序集的逻辑容器。CLR在初始化在初始化时创建第一个AppDomain(默认AppDomain),这个AppDomain在进程终止时被销毁。.NET的程序集正是在应用程序域中运行的。一个进程可以包含有多个应用程序域,一个应用程序域也可以包含多个程序集。
2,进程,线程和应用程序域的关系
可以用以下两幅图和两句话来总结。
1),一个进程可以包含多个线程和应用程序域。
2),一个线程可以穿梭在多个应用程序域中,但在某个时刻,线程只会处于一个应用程序域内。
前台线程和后台线程的区别
1,前台线程和后台线程的区别在于,应用程序必须运行完所有的前台线程才可以退出,而对于后台线程,可以不考虑其是否运行完而直接退出并且不会抛出异常,所有的后台线程在应用程序退出时就自动结束了。
2,默认情况下,主线程和使用Thread创建的线程都是前台线程(使用线程池和Task创建的线程默认都是后台线程),除非手动设置IsBackground= true。
多线程和异步的区别
多线程和异步在很多时候被认为是同一个东西,都是为了让主线程不需要等待而继续执行。
但是从辩证关系上来看,两者还是有区别的,可以用一句话来概括。
异步是目的,多线程是实现异步的其中的一种方式(比如还可以通过创建另一个进程实现异步)。