一、线程的诞生
1、早期操作系统单线程的糟糕使用体验
在早期的计算机操作系统中并没有多线程的概念。事实上,当时的操作系统整体只运行着一个执行主线程,其中同时运行着操作系统和应用程序的代码。只用一个主线程的问题在于,当某个任务需要耗费大量时间时会阻止后续任务的执行,例如某个窗口应用在进行长时间计算时,其窗口是不能移动的。更严重的情况,例如因为某些bug,主线程进入一个死循环时,会造成应用程序未响应,甚至会导致计算机停止工作。
遇到这类问题时,用户只好按Reset键或者直接强制重启计算机。更重要的是,其他正在排队的正常进程都会因此被同时杀死,并且正在处理的数据也会无端的丢失,这对使用体验来说简直是个灾难)。
为此,支持多线程控制的操作系统开发被各大操作系统厂商早早的提上日程。
2、以微软的windows系统为例,讲讲线程的基本设计
①进程——应用程序运行实例的容器
进程实际是应用程序的实例要使用的资源的集合。
每个进程都被赋予了一个虚拟地址空间,确保在一个进程中使用的代码和数据无法由另一个进程访问。这确保了应用程序的实例的健壮性。此外,进程访问不了OS的内核代码和数据,所以,应用程序代码也就破坏不了操作系统代码或数据。由于应用程序代码破坏不了其他应用程序或者OS本身,所以用户的使用体验变得更好了。除此之外,系统变得比以往更加安全,因为应用程序代码实际上只能访问到自身所在进程的内存,内容的访问权限及范围均被限制。
②线程——操作系统对CPU的虚拟化
当应用程序发生死循环时,如果当前计算机只有一个物理CPU,它的执行逻辑就会被死循环完全占据,导致不能执行其他东西,虽然进程保证了内存安全,但系统仍然可能因为死循环而停止响应。微软为了解决这个问题,他们拿出的解决方案就是“线程”。线程的职责是对物理CPU进行虚拟化。Windows为每个进程都提供了该进程专用的线程(功能相当于一个CPU),所以,在Windows操作系统中,一个进程至少有一个线程,一般也被称为主线程,但一个进程能拥有很多线程。
线程很强大,因为它能使OS即使在执行长时间运行的任务甚至是某个线程陷入死循环bug时也能随时响应。此外,线程允许用户使用一个应用程序(例如“任务管理器”)强制终止似乎已经冻结的应用程序。
假设此时操作系统中有两个进程A和B,且各有一个进程(在此就称为a1和b1)。当A进程的a1线程产生死循环时,若此时的操作系统是远古版本的单线程执行,则毫无疑问会造成线程阻塞,OS以及排在A进程后面的B进程都被物理CPU正在执行的死循环卡住了。
而在支持多线程的OS中,这样的情况几乎不会发生,因为当a1线程产生死循环后,A进程就会被操作系统“冻结”,转而去执行B进程的b1线程。亦或是能从容地打开“任务管理器”,将已经卡死的A进程直接kill掉。
综上所述,微软提出的“线程”解决方案有效的解决了早期单线程OS容易被bug卡死的痛点。
二、线程能够“并发”执行的原理——上下文切换
众所周知,单CPU计算机一次只能做一件事情,因此Windows必须在所以线程(逻辑CPU)之间共享物理CPU。
Windows任何时刻只将一个线程分配给一个CPU。被分配CPU的线程能运行一个“时间片”的长度。时间片到期,Windows就上下文切换到另一个线程,每次执行上下文切换时Windows都需要做:
1.将CPU寄存器的值保存到当前正在运行的**线程的内核对象(线程内核对象)**的一个上下文结构中。
2.从现有线程集合中选出一个线程供调度。如果该线程由另一个进程拥有,Windows在开始执行任何代码或者在接触任何数据之前,还必须切换CPU“看见”的虚拟地址空间。
3.将所选上下文结构中的值加载到CPU的寄存器中。
上下文切换完成后,CPU执行所选的线程,直到它的时间片到期。然后发生下一次上下文切换。Windows大约每30毫秒执行一次上下文切换。但是上下文切换是净开销,上下文切换所产生的开销不会换来任何内存或者性能上的收益。而Windows执行上下文切换,旨在向用户提供一个健壮的、响应灵敏的操作系统。
假如一个应用程序的线程进入死循环,Windows会定期抢占它,将新线程分配给CPU,让新线程有机会运行。假定新线程是“任务管理器”的线程,用户就可以利用“任务管理器”终止包含了死循环线程的进程。之后,进程会终止,它处理的所有数据会被销毁。但是其他正常的进程仍然继续运行,不会造成额外的损失。(所以,上下文切换通过牺牲部分性能换来了好得多的用户体验。)
通俗的讲,线程之所以能够“并发”执行,其实就是给每个进程的每个线程轮流发“CPU限时体验卡”,不论是提前结束亦或是到达期限,当前线程都会被OS给“暂停”,然后将“CPU限时体验卡”发给下一个线程。
三、CLR线程池基础
1.CLR简述
公共语言运行时(Common Language Runtime,简称CLR)是由微软开发的一个关键组件,用于支持和管理.NET框架中的应用程序的执行。CLR是.NET框架的一部分,它提供了许多重要的功能,包括以下方面:
跨语言互操作性:CLR允许不同编程语言(如C#、VB.NET、F#、C++/CLI等)编写的代码相互调用和交互。这意味着你可以在同一个项目中使用多种编程语言,以便充分利用各种语言的优势。
自动内存管理:CLR负责自动管理应用程序的内存,包括分配和释放内存。这有助于减少内存泄漏和提高应用程序的稳定性。
异常处理:CLR提供了强大的异常处理机制,允许开发人员捕获和处理运行时错误,从而增强了应用程序的健壮性。
安全性:CLR实施了严格的安全性措施,包括代码访问安全性、类型安全性和代码验证,以确保应用程序不会执行恶意代码。
JIT编译器:CLR包含一个即时编译器(Just-In-Time Compiler),它将中间语言(IL)代码编译成本机机器代码,以便在特定平台上运行。这有助于提高应用程序的性能。
线程管理:CLR允许开发人员创建和管理多线程应用程序,以实现并发执行和多任务处理。
类型系统:CLR提供了一个强大的类型系统,包括面向对象的编程概念,例如类、继承、接口等,以帮助开发人员创建可维护和可扩展的代码。
总之,CLR是.NET框架的核心组件之一,它提供了许多功能和服务,以简化开发人员创建和管理.NET应用程序的任务,并提高了应用程序的性能、安全性和稳定性。
2.线程池简述
线程池是一个线程管理机制,它维护了一组可用于执行任务的线程。这些线程可以被应用程序利用来执行异步操作,而不需要每次都创建和销毁线程,从而提高了性能和资源利用率。
3.CLR框架下的线程池
我们以Windows系统为例,因为操作系统必须调度可运行的线程并执行上下文切换,所以太多的线程会对性能不利(例如某个线程任务完成后其创建的线程没被回收则会造成线程冗余),为了改善这个情况,CLR包含了代码来管理它自己的线程池。
线程池是你的应用程序能使用的线程集合。每个CLR都拥有一个自己的线程池,这个线程池由CLR控制的所有AppDomain共享。如果一个进程加载了多个CLR,那么每个CLR都有它自己的线程池。
CLR初始化时,线程池中是没有线程的。在内部,线程池维护了一个操作请求队列。应用程序执行一个异步操作时,就调用某个方法,将一个记录项追加到线程池的队列中。线程池的代码从这个队列中提取记录项,将这个记录项派发给一个空闲的线程池线程。
如果线程池中没有线程,就创建一个新线程。创建线程会造成一定的性能损失。然而,当线程池完成任务后,线程不会立即销毁。相反,线程会返回线程池,在哪里进入空闲状态,等待响应另一个请求。由于线程不会立即销毁自身,所以不会再产生额外的性能损失。
如果你的应用程序向线程池发出许多请求,线程池会尝试只用一个线程来服务所有请求。然而,如果你的应用程序发出请求的速度超过了线程池线程处理它们的速度,就会创建额外的线程。最终,你的应用程序的所有请求都能由较少量的线程处理,所以线程池不必创建大量的线程。
如果你的应用程停止向线程池发出请求,线程池中会出现大量什么都不做的空闲线程。这是对内存资源的浪费。所以,当一个线程池线程闲着没事儿一段时间之后,线程会自己醒来终止自己以释放资源。线程终止自己会产生一定的性能消耗。然而,线程终止自己是本来就是因为它闲得慌,表明应用程序本身就没有做太多事情,所以这个性能消耗关系不大。
总结,线程池可以只容纳少量线程,从而避免浪费资源;也可以容纳更多的线程,以充分利用 多CPU、超线程CPU和多核CPU。它能在这两种不同的状态之间从容的切换。线程池是启发式的。如果应用程序需要执行许多任务,同时有可用的CPU运算资源,那么线程池会创建更多的线程。应用程序负载减轻,线程池中多余的空闲线程就终结他们自己以释放内存资源。
参考文献:《CLR Via C#(第四版)》——Jeffrey Richter
评论