CLR via C#, 4th -- 【线程处理】 -- 第26章线程基础

26.1 Windows为什么要支持线程

进程

Microsoft设计这个OS内核时,决定在一个进程中运行应用程序的每个实例。
进程实际是应用程序的实例要使用的资源的集合。每个进程都被赋予了一个虚拟地址空间,确保在一个进程中使用的代码和数据无法由另一个进程访问。
线程
作为一个Windows概念,线程的职责是对CPU进行虚拟化。
如果机器只有一个CPU,应用程序发生死循环,它会执行死循环,不能执行其他任何东西。所以,虽然数据无法被破坏,而且更安全,但系统仍然可能停止响应。Windows为每个进程都提供了该进程专用的线程(功能相当于一个CPU)。应用程序的代码进入死循环,与那个代码关联的进程会“冻结”,但其他进程(它们有自己的线程)不会冻结,它们会继续执行!

26.2 线程开销

线程有空间(内存耗用)和时间(运行时的执行性能)上的开销,每个线程都有以下要素:
线程内核对象(thread kernel object)
OS为系统中创建的每个线程都分配并初始化这种数据结构之一。数据结构包含一组对线程进行描述的属性。数据结构还包含所谓的线程上下文(thread context)。上下文是包含CPU寄存器集合的内存块。
线程环境块(thread environment block,TEB)
TEB是在用户模式(应用程序代码能快速访问的地址空间)中分配和初始化的内存块。
TEB耗用1个内存页(4KB),TEB包含线程的异常处理链首(head),线程进入的每个try块都在链首插入一个节点(node):线程退出try块时从链中删除该节点。此外,TEB还包含线程的“线程本地存储”数据,以及由GDI(Graphics Device Interface,图形设备接口)和OpenGL图形使用的一些数据结构。
用户模式栈(user-mode stack)
用户模式栈存储传给方法的局部变量和实参。它还包含一个地址;指出当前方法返回时,线程应该从什么地方接着执行。Windows默认为每个线程的用户模式栈分配1MB内存,在线程实际需要时才会提交(调拨)物理内存。
内核模式栈(kernel-mode stack)
应用程序代码向操作系统中的内核模式函数传递实参时,还会使用内核模式栈。出于对安全的考虑,针对从用户模式的代码传给内核的任何实参,Windows都会把它们从线程的用户模式栈复制到线程的内核模式栈。
DLL线程连接(attach)和线程分离(detach)通知
Windows的一个策略是,任何时候在进程中创建线程,都会调用进程中加载的所有非托管DLL的DIMain方法,并向该方法传递DLL-THREAD-ATTACH标志。

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

Windows任何时刻只将一个线程分配给一个CPU,那个线程能运行一个“时间片”(“量”/“量程”,quantum)的长度。时间片到期,Windows就上下文切换到另一个线程。

每次上下文切换Windows执行的操作:
1,将CPU寄存器的值保存到当前正在运行的线程的内核对象内部的一个上下文结构中。
2,从现有线程集合中选出一个线程供调度。如果该线程由另一个进程拥有,Windows在开始执行任何代码或者接触任何数据之前,还必须切换CPU“看见”的虚拟地址空间。
3,将所选上下文结构中的值加载到CPU的寄存器中。

执行垃圾回收时,CLR必须挂起(暂停)所有线程,遍历它们的栈来查找根以便对堆中的对象进行标记,再次遍历它们的栈(有的对象在压缩期间发生了移动,所以要更新它们的根),再恢复所有线程。所以,减少线程的数量也会显著提升垃圾回收器的性能。每次使用调试器并遇到断点,Windows都会挂起正在调试的应用程序中的所有线程,并在单步执行或者运行应用程序时恢复所有线程。所以,线程越多,调试体验越差。

Windows为每个CPU内核都分配一个线程,每个内核都自己执行到其他线程的上下文切换。Windows确保单个线程不会同时在多个内核上调度。

26.3 停止疯狂

“任务管理器”并选择“性能”选项卡,查看电脑的进程和线程数量

26.4 CPU发展趋势

今天的计算机使用了以下三种多CPU技术
多个CPU
有的计算机安装了多个CPU,主板上有多个CPU插座,每个都可安装一个CPU。
超线程芯片
这种技术(Intel专利)允许一个芯片在操作系统中显示成两个。芯片中包含两组架构状态(比如CPU寄存器),但芯片只有一组执行资源。对于Windows,这看起来是安装了两个CPU,所以Windows会同时调度两个线程。但芯片一次只能执行一个线程。
多核芯片
包含多个内核的CPU芯片,双核、四核和八核CPU已经很流行。

26.5 CLR线程和Windows线程

CLR线程完全等价于Windows线程

26.6 使用专用线程执行异步的计算限制操作

满足以下任何条件,就可显式创建自己的线程

  • 线程需要以非普通线程优先级运行。所有线程池线程都以普通优先级运行;虽然可以更改这个优先级,但不建议那样做。另外,在不同的线程池操作之间,对优先级的更改是无法持续的。
  • 需要线程表现为一个前台线程,防止应用程序在线程结束任务前终止。
  • 计算限制的任务需要长时间运行。
  • 要启动线程,并可能调用Thread的Abort方法来提前终止它。

为了创建专用线程,要构造System.Threading.Thread类的实例,向构造器传递一个方法名。以下是Thread的构造器的原型:

public sealed class Thread : CriticalFinalizerObject, ... {  
   public Thread(ParameterizedThreadStart start);  
   // Less commonly used constructors are not shown here    
}

start参数标识专用线程要执行的方法,这个方法必须和ParameterizedThreadStart委托的签名匹配:

delegate void ParameterizedThreadStart(Object obj);

Thread还提供了一个获取ThreadStart委托的构造器。ThreadStart委托不接受任何实参,返回void,我个人不建议使用这个构造器和委托,因为它们的功能十分有限。如果你的线程方法(要在线程上执行的方法)要获取一个Object并返回void,可以使用一个专用线程来调用,也可以使用线程池(第27章会解释具体做法)。

using System;  
using System.Threading;  
  
public static class Program {  
   public static void Main() {   
      Console.WriteLine("Main thread: starting a dedicated thread " +  
         "to do an asynchronous operation");  
      Thread dedicatedThread = new Thread(ComputeBoundOp);  
      dedicatedThread.Start(5);  
  
      Console.WriteLine("Main thread: Doing other work here...");   
      Thread.Sleep(10000);     // Simulating other work (10 seconds)  
  
      dedicatedThread.Join();  // Wait for thread to terminate   
      Console.WriteLine("Hit <Enter> to end this program...");   
      Console.ReadLine();   
   }  
  
   // This method's signature must match the ParameterizedThreadStart delegate  
   private static void ComputeBoundOp(Object state) {  
      // This method is executed by a dedicated thread    
  
      Console.WriteLine("In ComputeBoundOp: state={0}", state);  
      Thread.Sleep(1000);  // Simulates other work (1 second)  
  
      // When this method returns, the dedicated thread dies   
   }  
}

注意,Main方法调用了Join.Join方法造成调用线程阻塞(暂停)当前执行的任何代码,直到dedicatedThread所代表的那个线程销毁或终止。

26.7 使用线程的理由

可响应性(通常是对于客户端GUI应用程序)
Windows为每个进程提供它自己的线程,确保发生死循环的应用程序不会妨碍其他应用程序。
在自己的客户端GUI应用程序中,可以将一些工作交给一个线程进行,使GUI线程能灵敏地响应用户输入。
性能(对于客户端和服务器应用程序)
由于Windows每个CPU调度一个线程,而且多个CPU能并发执行这些线程,所以同时执行多个操作能提升性能。

26.8线程调度和优先级

抢占式(preemptive)操作系统必须使用算法判断在什么时候调度哪些线程多长时间。

Windows选择一个可调度的线程内核对象,并上下文切换到它。Windows实际记录了每个线程被上下文切换到的次数。可以使用像Microsoft Spyt这样的工具查看这个数据。(Microsoft Spy+(spyxx.exe)随同Visual Studio安装。启动“VS2013开发人员命令提示”,输入spyxx即可启动它。)

Windows之所以被称为抢占式多线程(preemptive multithreaded)操作系统,是因为线程可在任何时间停止(被抢占)并调度另一个线程。

每个线程都分配了从0(最低)到31(最高)的优先级。

饥饿(starvation)
只要存在可调度的优先级31的线程,系统就永远不会将优先级0~30的任何线程分配给CPU。

较高优先级的线程总是抢占较低优先级的线程,无论正在运行的是什么较低优先级的线程。如果系统确定有一个较高优先级的线程准备好运行,系统会立即挂起(暂停)较低优先级的线程(即使后者的时间片还没有用完),将CPU分配给较高优先级的线程,该线程将获得一个完整的时间片。

零页线程(zero page thread)
系统启动时会创建一个特殊的零页线程。该线程的优先级是0,而且是整个系统唯一优先级为0的线程。在没有其他线程需要“干活儿”的时候,零页线程将系统RAM的所有空闲页清零。

进程优先级类(priority class)
Windows支持6个进程优先级类:Idle,Below Normal,Normal,Above Normal,High和Realtime。默认的Normal是最常用的优先级类。
只有绝对必要的时候才应使用High优先级类。Realtime优先级类要尽可能地避免。Realtime优先级相当高,它甚至可能干扰操作系统任务,比如阻碍一些必要的磁盘10和网络传输。
优先级类和优先级是两个概念。根据定义,每个线程的优先级取决于两个标准:1)它的进程的优先级类;2)在其进程的优先级类中,线程的优先级。优先级类和优先级合并构成了一个线程的“基础优先级”(base priority)
注意,每个线程都有一个动态优先级(dynamic priority),线程调度器根据这个优先级来决定要执行哪个线程。最初,线程的动态优先级和它的基础优先级是相同的。系统可提升(boost)和降低(lower)动态优先级,以确保它的可响应性,并避免线程在处理器时间内“饥饿”。但是,对于基础优先级16-31之间的线程,系统不会提升它们的优先级。只有基础优先级在0到15之间的线程才会被动态提升(优先级)。

注意“进程优先级类”的概念容易引起混淆。人们可能以为Windows在调度进程。事实上Windows永远不会调度进程,它只调度线程。“进程优先级类”是Microsoft提出的抽象概念,旨在帮助你理解自己的应用程序和其他正在运行的应用程序的关系,它没有别的用途。

相对线程优先级

Windows支持7个相对线程优先级:Idle,Lowest,Below Normal,Normal,Above Normal,Highest和Time-Critical,这些优先级是相对于进程优先级类而言的。

TABLE 26-1  H ow Process Priority Class and Relative Thread Priorities Map to Priority Levels

Relative Thread Priority

Process Priority Class

Idle

Below  

Normal

Above Normal

High

Realtime

Time-Critical

15

Normal

15

15

15

31

Highest

6

15

10

12

15

26

Above Normal

5

8

9

11

14

25

Normal

4

7

8

10

13

24

Below Normal

3

6

7

9

12

23

Lowest

2

5

6

8

11

22

Idle

1

4

1

1

1

16

注意,没有值为0的线程优先级。这是因为0优先级保留给零页线程了,系统不允许其他线程的优先级为0。而且,以下优先级也不可获得:17,18,19,20,21,27,28,29或者30。以内核模式运行的设备驱动程序才能获得这些优先级;用户模式的应用程序不能。还要注意,Realtime优先级类中的线程优先级不能低于16,类似地,非Realtime的优先级类中的线程优先级不能高于15。

重要提示 最好是降低一个线程的优先级,而不是提升另一个线程的优先级。如果线程要执行长时间的计算限制任务,比如编译代码、拼写检查、电子表格重新计算等,一般应降低该线程的优先级。如果线程要快速响应某个事件,运行短暂时间,再恢复为等待状态,则应提高该线程的优先级。

"Windows资源管理器”线程就是一个高优先级线程。

另外,你的应用程序可更改其线程的相对线程优先级,需要设置Thread的Priority属性,向其传递ThreadPriority枚举类型定义的5值之一:Lowest,BelowNormal,Normal,AboveNormal或者Highest,然而,就像Windows为自己保留了优先级0和Realtime范围一样,CLR为自己保留了Idle和Time-Critical优先级。

对于桌面应用(非Windows Store应用),System.Diagnostics命名空间包含Process类和ProcessThread类。这两个类分别提供了进程和线程的Windows视图。应用程序需要以特殊的安全权限运行才能使用这两个类。例如,Silverlight或ASP.NET应用程序便用不了这两个类。

26.9 前台线程和后台线程

CLR将每个线程要么视为前台线程,要么视为后台线程。一个进程的所有前台线程停止运行时,CLR强制终止仍在运行的任何后台线程。这些后台线程被直接终止;不抛出异常。
应该用前台线程执行确实想完成的任务,比如将数据从内存缓冲区flush"到磁盘。非关键性任务则使用后台线程,比如重新计算电子表格的单元格,或者为记录建立索引等。这是由于这些工作能在应用程序重启时继续,而且如果用户想终止应用程序,就没必要强迫应用程序保持活动。

using System;  
using System.Threading;  
 
public static class Program { 
   public static void Main() { 
      // Create a new thread (defaults to foreground)  
      Thread t = new Thread(Worker);  
 
      // Make the thread a background thread 
      t.IsBackground = true;  
 
      t.Start(); // Start the thread  
      // If t is a foreground thread, the application won't die for about 10 seconds 
      // If t is a background thread, the application dies immediately 
      Console.WriteLine("Returning from Main"); 
   } 
 
   private static void Worker() {  
      Thread.Sleep(10000);  // Simulate doing 10 seconds of work 
 
      // The following line only gets displayed if this code is executed by a foreground thread 
      Console.WriteLine("Returning from Worker"); 
   } 
}

在线程的生存期中,任何时候都可以从前台变成后台,或者从后台变成前台。应用程序的主线程以及通过构造一个Thread对象来显式创建的任何线程都默认为前台线程。相反,线程池线程默认为后台线程。另外,由进入托管执行环境的本机(native)代码创建的任何线程都被标记为后台线程。

power-threading-library

posted @ 2019-11-07 18:30  FH1004322  阅读(125)  评论(0)    收藏  举报