线程机制、CLR线程池以及应用程序域

最近在总结多线程、CLR线程池以及TPL编程实践,重读一遍CLR via C#,比刚上班的时候收获还是很大的。还得要多读书,读好书,同时要多总结,多实践,把技术研究透,使用好。

话不多说,直接上博文吧。先说一下,为什么Windows要支持线程机制?

1. Windows为什么要支持线程

计算机的早期时代,操作系统没有线程的概念,整个系统只运行着一个执行线程,其中包含操作系统代码和应用程序代码。只用一个执行线程的问题在于,长时间运行的任务会阻止其他任务的执行。例如16位Windows的时代,打印文档的应用程序很容易“冻结”整个机器。

Microsoft 在设计Windows NT这个版本的OS内核时,决定在一个进程中运行应用程序的每个实例。进程实际是应用程序的实例要使用的资源的集合。每个进程都被赋予了一个虚拟地址空间,确保一个进程中使用的代码和数据无法由另一个进程访问。这就确保了应用程序实例的健壮性。同时,进程访问不了OS的内核代码和数据;所以,应用程序代码破坏不了操作系统的代码和数据

如果应用程序发生死循环会发生什么?如果机器只有一个CPU,它会执行死循环,不能执行其他任何程序。Microsoft 的解决方案就是线程。作为一个Windows概念,线程的职责是对CPU进行虚拟化。Windows为每个进程都提供了该进程专用的线程(功能相当于一个CPU)。应用程序的代码进行死循环,与代码关联的进程会“冻结”,但其他进程(它们有自己的线程)不会冻结,它们会继续执行。
线程很强大,因为它们使Windows即使在执行长时间运行的任务时,也能随时响应。

但是,和一切虚拟化机制一样,线程有空间(内存消耗)和时间(运行时的执行性能)的开销。

2. 线程开销有哪些?

每个线程都有以下要素组成:

线程内核对象(thread kernel object):线程的描述属性和线程上下文,上下文是包含CPU寄存器集合内存块。对于x86、x64、ARM CPU架构,线程上下文分别使用约700,1240和350字节的内存。

线程环境块(thread environment block,TEB):用户模式(应用程序代码能快速访问的地址空间)中分配和初始化的内存块。TEB耗用一个内存页( x86、x64、ARM CPU 中是4KB)

用户模式栈(user-mode stack):用户模式栈存储传给方法的局部变量和实参,它还包含一个地址:指出当前方法返回时,线程应该从什么地方接着执行。Windows默认为每个线程的用户模式栈分配1MB内存。Windows只是保留1MB地址空间,在线程实际需要时才会提交(调拨)物理内存。

内核模式栈(kernel-mode stack):应用程序代码向操作系统中的内核模式传递参数时,还会使用内核模式栈,出于安全的考虑,Windowd会把这些实参从线程的用户模式栈复制到线程的内核模式栈。32windows 内核模式栈大小12KB,64位是24KB

DLL线程连接(Attach)和线程分离(Detach)通知:Windows的一个策略是,任何时候在进程中创建线程,都会调用进程中加载的所有非托管DLL的DllMain方法,并向该方法传递DLL_THREAD_ATTACH标志。同样的,任何时候线程终止,都会调用进程中的所有非托管DLL的DllMain方法,并向该方法传递DLL_THREAD_DETACH标志。

上下文切换

单CPU计算机一次只能做一件事情。所以,Windows必须在操作系统中的所有线程(逻辑CPU)之间共享物理CPU。

Windows任何时刻只将一个线程分配给一个CPU。那个线程能运行一个“时间片”的长度。时间片到期,Windows就将上下文切换到另一个线程。每次上下文切换都要求Windows执行一下操作:

  • 将CPU寄存器的值保存到当前正在运行的线程的内核内部的一个上下文结构中
  • 从现有线程集合中选出一个线程供调度
  • 将所选上下文结构中的值加载到CPU寄存器中

当Windows上下文切换到另一个线程时,会产生一定的性能损失。

一个时间片结束时,如果Windows决定再次调度同一个线程(而不是切换到另一个线程),那么Windows不会执行上下文切换。

3. 使用线程的理由:什么场景下使用线程

可响应性(通常是针对客户端GUI应用程序):客户端GUI应用程序中,可以将一些工作交给一个线程进行,使GUI线程能灵敏的响应用户的输入。在这个过程中创建的线程数可能超过CPU的核心数,会浪费系统资源和降低性能。但是用户体验得到改善和增强。

性能(对于客户端和服务端应用程序):由于Windows每个CPU调度一个线程,而且多个CPU能并发执行这些线程,所以同时执行多个操作可以提升性能。

4. CLR线程池机制

创建和销毁线程是一个昂贵的操作,要耗费大量的时间,同时,太多的线程会浪费内存资源。由于操作系统必须调度可运行的线程并执行上下文切换,所以太多的线程会影响性能。

为了改善这个情况,CLR提供了线程池机制,每个CLR一个线程池

CLR线程池并不会在CLR初始化的时候立刻建立线程,而是在应用程序要创建线程来执行任务时,线程池才初始化一个线程。线程的初始化与其他的线程一样。在完成任务以后,该线程不会自行销毁,而是以挂起的状态返回到线程池。直到应用程序再次向线程池发出请

求时,线程池里挂起的线程就会再度激活执行任务。

这样既节省了创建线程所造成的性能损耗,也可以让多个任务反复重用同一线程,从而在应用程序生存期内节约大量开销。

5. 进程、线程和应用程序域

进程(Process):Windows系统中的一个基本概念,它包含着一个运行程序所需要的资源。进程之间是相对独立的,一个进程无法访问另一个进程的数据(除非利用分布式计算方式),一个进程运行的失败也不会影响其他进程的运行,Windows系统就是利用进程把工作划分为多个独立的区域的。进程可以理解为一个程序的基本边界。

应用程序域(AppDomain):一组程序集的逻辑容器。CLR在初始化在初始化时创建第一个AppDomain(默认AppDomain),这个AppDomain在进程终止时被销毁。.NET的程序集正是在应用程序域中运行的。

一个进程可以包含有多个应用程序域,一个应用程序域也可以包含多个程序集。

在一个应用程序域中包含了一个或多个上下文context,使用上下文CLR就能够把某些特殊对象的状态放置在不同容器当中

线程(Thread):进程中的基本执行单元,在进程入口执行的第一个线程被视为这个进程的主线程。在.NET应用程序中,都是以Main()方法作为入口的,当调用此方法时系统就会自动创建一个主线程。

线程主要是由CPU寄存器、调用栈和线程本地存储器(Thread Local Storage,TLS)组成的。CPU寄存器主要记录当前所执行线程的状态,调用栈主要用于维护线程所调用到的内存与数据,TLS主要用于存放线程的状态信息。

6. 进程、线程和应用程序域的关系

进程、应用程序域、线程的关系如下图,

一个进程内可以包括多个应用程序域,也有包括多个线程,线程也可以穿梭于多个应用程序域当中。但在同一个时刻,线程只会处于一个应用程序域内。

周国庆

2017/5/26

时间: 2024-10-12 12:36:53

线程机制、CLR线程池以及应用程序域的相关文章

基础线程机制--Executor线程池框架

基础线程机制 Executor线程池框架 1.引入Executor的原因 (1)new Thread()的缺点 ???每次new Thread()耗费性能 ???调用new Thread()创建的线程缺乏管理,被称为野线程,而且可以无限制的创建,之间相互竞争,导致过多的系统资源被占用导致系统瘫痪,不利于定时执行,定期执行,线程中断. (2)采用线程池的优点 ???可以重用创建的线程,减少对象的创建,消亡的开销,性能更佳. ???可以有效的控制最大并发线程数,提高系统资源的利用率,避免过多的资源竞

linux网络编程学习笔记之五 -----并发机制与线程池

进程线程分配方式 简述下常见的进程和线程分配方式:(好吧,我只是举几个例子作为笔记...并发的水太深了,不敢妄谈...) 1.进程线程预分配 简言之,当I/O开销大于计算开销且并发量较大时,为了节省每次都要创建和销毁进程和线程的开销.可以在请求到达前预先进行分配. 2.进程线程延迟分配 预分配节省了处理时的负担,但操作系统管理这些进程线程也会带来一定的开销.由此,有个折中的方法是,当某个处理需要花费较长时间的时候,我们创建一个并发的进程或线程来处理该请求.实现也很简单,在主线程中定时,定时到期,

【Java 并发】Executor框架机制与线程池配置使用

[Java 并发]Executor框架机制与线程池配置使用 一,Executor框架Executor框架便是Java 5中引入的,其内部使用了线程池机制,在java.util.cocurrent 包下,通过该框架来控制线程的启动.执行和关闭,可以简化并发编程的操作.因此,在Java 5之后,通过Executor来启动线程比使用Thread的start方法更好,更易管理,效率更好(用线程池实现,节约开销). Executor框架主要包括:Executor,Executors,ExecutorSer

补充:垃圾回收机制、线程池和ORM缺点

补充:垃圾回收机制.线程池和ORM缺点 垃圾回收机制不仅有引用计数,还有标记清除和分代回收 引用计数就是内存地址的门牌号,为0时就会回收掉,但是会出现循环引用问题,这种情况下会导致内存泄漏(即不会被用,也不会被清除,一直占着) 标记清除就是有引用就画有向图,如果根对象不能通过边达到,那么就被视为垃圾进行回收 分代回收就是把变量分为0代,1代等若干代,数字越小,表示越年轻,越容易被回收掉,0代通过一定的积累就会变成1代以此类推,数字大的一般是全局变量这样 详细戳视频 线程池:为了保障硬件的安全性,

CLR线程概览(下)

作者:施懿民链接:https://zhuanlan.zhihu.com/p/20866017来源:知乎著作权归作者所有.商业转载请联系作者获得授权,非商业转载请注明出处. 同步: 托管代码 托管代码可以访问很多在System.Threading里定义的同步原语.包括操作系统原语的简单封装如:互斥(Mutex),事件(Event)和旗标(Semaphore)对象,也包括类似的栅栏(Barrier)和自旋锁(SpinLock)等抽象.但托管代码用的最多的同步机制是System.Threading.M

CLR线程概览(一)

托管 vs. 原生线程 托管代码在“托管线程”上执行,(托管线程)与操作系统提供的原生线程不同.原生线程是在物理机器上执行的原生代码序列:而托管线程则是在CLR虚拟机上执行的虚拟线程. 正如JIT解释器将“虚拟的”中间(IL)指令映射到物理机器上的原声指令,CLR线程基础架构将“虚拟的”托管线程映射到操作系统的原生线程上. 在任意时刻,一个托管线程可能会也可能不会被分配到一个原生线程执行.例如,一个已经被创建(通过“new System.Threading.Thread”)但是未启动(通过“Sy

黑马程序员——JAVA基础之Day24 多线程 ,死锁,线程间通信 ,线程组,线程池,定时器。

------- android培训.java培训.期待与您交流! ---------- Lock()实现提供了比使用synchronized方法和语句可获得更广泛的锁定操作. private Lock lock =new ReentrantLock(); 被锁的代码要用   lock.lock()                lock.unlock()    包括.其中用try   ...finally包围 同步:效率低,如果出现同步嵌套,会出现死锁.  但是安全. 死锁问题:两个或者两个以上

线程基础:线程池(7)——基本使用(下)

(接上文<线程基础:线程池(6)--基本使用(中)>,我要加快进度,以便在2月份恢复"系统间通信技术"专栏的写作) 5.扩展ThreadPoolExecutor线程池 实际上JAVA中提供的ThreadPoolExecutor线程池是鼓励各位程序员进行扩展的(虽然大多数情况下您无需扩展),并且JBOSS(Netty).Apache(Camel)也正是这样在做.下面我们看看一些由ThreadPoolExecutor提供的扩展方式. 5-1.Hook methods 在Thre

【Java线程】Java线程池ExecutorService

示例 import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; public class Ch09_Executor { private static void run(ExecutorService threadPool) { for(int i = 1; i < 5; i++)