CLR Via C# 3rd 阅读摘要 -- Chapter 25 – Thread Basics
Why Does Windows Support Threads?
- Windows支持线程可以给系统提供更好的健壮性和交互响应;
- Windows一个进程至少有一个主线程,可以启动多个线程,调度单位是线程。
Thread Overhead
- 跟所有的虚拟化机制一样,线程有时间(运行时执行性能)和空间(内存)的额外开销;
- 每个线程会包括下列的个别内容:
- 线程核心对象:一个数据结构,包括一组描述线程的属性。以及调用的线程的上下文信息(一块内存用来放置CPU的寄存器状态),x86 CPU (≈700Bytes),x64 CPU(≈1240Bytes),IA64 CPU(≈2500Bytes);
- 线程环境块(TEB):在用户态(应用代码可以快速访问的地址空间)分配和初始化的内存块。TEB消耗一个内存页面,x86/x64 CPUs(4KB),IA64 CPU(8KB)。包括异常处理链的头,线程进入每一个try块时会在异常处理链的头部插入一个节点。另外TEB还包括线程的线程本地存储数据;
- 用户态堆栈:用来存储传到方法的本地变量和参数。Windows默认分配1MB;
- 核心态堆栈:当操作系统中应用程序代码传递给核心态函数的参数值。32位系统(12KB),64位系统(24KB);
- DLL线程进入离开通知:Windows有一个策略,任何时候进程中创建了线程,所有进程中装载的DLL都会调用DllMain方法,附带DLL_THREAD_ATTACH标志。同样的,在线程结束时,DllMain再次被调用,附带DLL_THREAD_DETACH标志。
- DisableThreadLibraryCalls,非托管代码调用该Win32函数可以忽略DLL_THREAD_ATTACH和DLL_THREAD_DETACH通知。C#和其他托管代码开发的DLL没有DllMain所以没有此缺陷;
- 上下文切换:分配给线程的CPU时间片配额用完后,Windows会切换到其他线程执行。会执行以下动作:
- 1.保存CPU寄存器的值到当前运行的线程的核心对象数据结构中;
- 2.从现存的线程集中调度一个线程。如果该线程属于其他进程,Windows还必须切换虚拟地址空间;
- 3.从目前调度的线程核心对象数据结构中装载值到寄存器。
- 当CPU时间片用完后,Windows可能会调度同一个线程,这种情况下不会执行上下文切换;
- 线程可以自愿的提前结束时间片,经常发生在I/O操作(键盘、网络、鼠标、……)完成时;
- 当执行垃圾收集时,CLR必须挂起所有的线程完成一系列检查清理操作。因此避免多线程可以极大的改进垃圾收集器的效率;
- 当使用Debugger时,Windows也会挂起所有的线程;
- 多CPU的计算机可以同时运行多个线程,增加了扩展性;
- Windows保证不会同时将一个线程调度到不同的CPU核上,因为这将引起灾难。
Stop the Madness
- 虽然创建线程的开销比进程小的多,但是跟系统资源比较起来还是很昂贵的,所以应该避免滥用。
CPU Trends
- 三种主流多CPU技术:一台计算机多片CPU、超线程CPU、多核CPU。
NUMA Architecture Machines
- ccNUMA: Cache-Coherent Non-Uniform Memory Access(连续缓存的非均匀存储器存取)。设计用于多处理器的计算机内存,访问内存时依赖于处理器相关的内存定位。在NUMA情况下,处理器访问自有的本地内存要比非本地(其他处理器共享的)内存快很多;
- NUMA的体系结构:
- 北桥芯片主要决定主板的规格、对硬件的支持、以及系统的性能,它连接着 CPU 、内存、 AGP 总线。主板支持什么 CPU ,支持 AGP 多少速的显卡,支持何种频率的内存,都是北桥芯片决定的。北桥芯片往往有较高的工作频率,所以发热量颇高,我们在主板上,可以在 CPU 插槽附近找到一个散热器,下面的就是北桥芯片。同北桥芯片的主板,性能差别微乎其微;
- 南桥芯片主要决定主板的功能,主板上的各种接口(如串口、 USB )、 PCI 总线(接驳电视卡、内猫、声卡等)、 IDE (接硬盘、光驱)、以及主板上的其他芯片(如集成声卡、集成 RAID 卡、集成网卡等),都归南桥芯片控制。南桥芯片通常裸露在 PCI 插槽旁边,块头比较大;
- 一台计算机可以有一个或多个处理器组,每个处理器组有一个或多个NUMA节点,每个NUMA节点包含多个逻辑处理器、高速缓存、本地内存,每个NUMA节点有一个或多个插槽芯片,每个插槽芯片有一个或多个CPU核,每个CPU核包含一个或多个逻辑处理器。Windows 2008支持256个逻辑处理器;
- 目前CLR还没有利用处理器组的优点,所有的线程都创建在0组(默认),并且在64位Windows上最多只支持64核。
CLR Threads and Windows Threads
- 目前CLR线程直接应射到Windows线程,但是以后未必会这样,CLR有可能引入自己的逻辑线程概念;
- 如果需要P/Invoke来最多限度的使用本地代码,使用当前的物理操作系统线程是非常重要的,这需要调用System.Threading.Thread.BeginThreadAffinity()静态方法。当不再需要使用物理线程时,调用Thread.EndThreadAffinity()通知CLR。
Using a Dedicated Thread to Perform an Asynchronous Compute-Bound Operation
- 本节的内容不建议使用,而推荐优先使用CLR的线程池;
- 如果要创建自己的线程而不采用线程池,先检查以下条件是否存在:
- 线程需要运行在非常规权限下。所有的线程池线程运行在正常权限下;
- 需要线程以前台线程方式运行,避免线程完成任务之前应用程序就over了。所有的线程池线程都是后台模式的;
- 计算密集任务需要很长时间运行;
- 有可能需要提前Abort的线程。
- 如果创建带参数的线程,使用System.Threading.Thread(ParameterizedThreadStart start);构造函数,不推荐使用;
Reasons to Use Threads
- 使用线程的理由:
- 隔离代码;
- 简化代码;
- 并发执行。
- 多线程可以充分利用CPU,但是如果想压榨计算机的CPU资源,有两个警告:
- 1.如果用电池供电的话,100%的cpu利用会很耗电;
- 2.一些数据中心宁愿10台计算机运行在50%的CPU负载,也不愿意5台计算机运行在100%的CPU负载。因为过热需要HVAC降温因此支付更多的电费。
Threading Scheduling and Priorities
- Windows为什么叫抢占式多线程操作系统,因为任何时候一个线程停止后,另外一个线程可以马上被调度;
- 线程的优先级别从0到31级,其中0, 17~30普通开发者不能使用;
- 进程优先级(Idle -> Below Normal -> Normal -> Above Normal -> High -> Realtime)与线程优先级(Idle -> Lowest -> Below Normal -> Normal ->Above Normal -> Highest -> Time-Critical)的关联矩阵:
- CLR中,ThreadPriority枚举只包括{Lowest, Below Normal, Normal, Above Normal, Highest}五种;
- 垃圾回收器线程以Time-Critical级别运行;
- System.Diagnotics下面有两个类:Process和ProcessThread,分别提供进程和线程的情况。
Foreground Threads versus Background Threads
- 如果进程中的所有前台线程停止运行,CLR强制结束仍然在运行的后台线程。这些后台线程立即结束,没有异常抛出;
- 如果希望线程执行的任务真正完成(比如刷新缓冲数据到物理磁盘),那么可以使用前台线程;
- 如果是无关紧要的任务(比如重新计算工作表),那么可以使用后台线程;
- 创建线程时,默认情况下是前台线程,如何转换成后台线程:Thread t = new Thread(...); t.IsBackground = true;
- 一般来说,应该尽可能的避免使用前台线程,因为这可能会阻止进程正常终止。
What Now?
- CLR的线程池是应该优先使用的;
- Wintellect Power Threading Library:可以简化线程同步和异步编程。
本章小结
本章讲述了线程的基本知识。首先介绍了Windows的线程概念,CPU的发展趋势,CLR线程与Windows线程直接的关系。然后分析了使用线程的开销,总结了在什么情况下使用线程,使用线程时要注意哪些问题。接着讲解了NUMA的体系结构。讲述了在什么情形下需用使用确定的线程来完成异步计算密集型任务。接着给出了使用线程的理由。重点深入解释了Windows线程的调度机制,线程的优先级以及和线程优先级之间的关联。本章的结尾讲述了选择前台线程和后台线程的条件。最后还推荐了Wintellect Power Threading Library来简化异步编程和线程同步。