《CLR Via C#》读书笔记:26.线程基础

一、线程开销

操作系统创建线程是有代价的,其主要开销在下面列举出来了。

内存开销

  1. 线程内核对象

    拥有线程描述属性与线程上下文,线程上下文占用的内存空间为 x86 架构 占用 700 字节、x64 架构 1240 字节 、ARM 架构 350 字节。

  2. 线程环境块(TEB)

    TEB 消耗一个内存页,占用 4KB内存。

  3. 用户模式栈。

    用户模式栈存储传递给方法的局部变量与实参,并且还存储有一个地址用于当前方法返回的时候,线程应该从哪个地方继续执行。默认 Windows 分配保留 1MB 内存。

  4. 内核模式栈。

    32 位 Windows 占用 12 KB,64 位 Windows 占用 24 KB。

  5. DLL 线程连接与线程分离通知。

    这种策略只有 Windows 才会存在,当创建线程时, Windows 会调用进程所有非托管 DLL 的 DllMain 方法,并未其传递 DLL_THREAD_ATTACH 标志,线程终止时传递 DLL_THREAD_DETACH 标志。

线程上下文切换与 CPU 之间的关系

Windows 在任何时刻都只会将 1 个线程分配给 1 个 CPU ,该线程享有一个时间片的运行时间。时间片到期之后,Windows 会将上下文切换到另外一个线程,动作如下:

  1. 将 CPU 寄存器值存储在当前正在运行的线程的内核对象内部的上下文结构之中。
  2. 从先有线程集合选取一个线程供调度,如果该线程属于另一个进程,还得切换 CPU 能够操作的虚拟地址空间。
  3. 将上下文结构中的值加载到 CPU 寄存器之中。

以上操作做完之后,Windows 等待这个线程时间片到期,执行下次切换,每次切换的时间开销大概为 30 毫秒。

如果一个线程时间片结束之后,下一个调度的线程还是之前的线程则不会产生线程上下文切换。

所以在理想状态下,每个系统最佳的线程数应该与其核心数相同,(如果是 4 核 8 线程则最优应该为 8 个)因为这样上下文切换出现的情况就会少很多。

最重要的是,Windows 系统上大部分程序线程都处于空闲状态,但是线程占用的内存空间是事实存在的。

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

一般来说不推荐使用 Thread 手动创建线程,而应该使用线程池,不过在有以下需求时,可以手动创建线程。

  1. 需要设定更高的线程优先级的时候。
  2. 需要将线程设置为前台线程。
  3. 某些长耗时的专用线程。
  4. 该线程可能会通过 Thread 的 Abort 方法终止自身。

在调用过程中,如果使用了 Thread.Join() 方法那么就会造成调用线程阻塞当前代码,直到创建的线程被终止。

四、为什么要使用线程

  1. 针对于客户端程序而言,多线程可以增强响应性,不会因为耗时操作阻塞 UI 线程造成用户体验卡顿。
  2. 针对于服务器程序而言,可以并发地处理用户请求,充分利用多核 CPU 的优势。

作者的观点是,计算机的 CPU 使用率应该保持 100% 的使用率才不算是浪费计算资源。

五、线程调度与优先级

抢占式系统通过优先级来判定线程在什么时候调度多少时间,每个线程都分配了从 0 到 31 的优先级,系统为 CPU 分配线程时,首先检查 31 的线程,并以轮询的方式调度他们(优先级都为 31)。

如果高优先级的线程一直处于调度状态,那么操作系统不会将 CPU 分配给低优先级的线程,这样就会造成 线程饥饿

较高的优先级线程总会抢占低优先级线程,即便该线程的时间片没有用完。

CPU 会创建一个优先级为 0 的 零页线程 ,该线程是系统唯一一个优先级为 0 的线程,只有在 CPU 空闲的时候会执行他,用于清理 RAM 中所有的空闲内存页。

【注意】

进程优先级类 + 线程优先级构成了一个基础优先级,Windows 还有一个动态优先级用于防止产生线程饥饿,会动态调成线程的优先级状态。

但是动态优先级只会针对基础优先级在 0 ~ 15 的线程应用,16 ~ 31 不受这个管控。

Windows 通过两个抽象层用于表示进程优先级类和线程优先级,单一般 C# 用户代码中能够控制的只有线程优先级,他们分别是:Lowest、BelowNormal、Normal、AboveNormal、Highest。

六、前台线程与后台线程

在 CLR 中线程只有两种状态,前台线程和后台线程,而且当所有前台线程被终止之后,CLR 会强行关闭所有后台线程,并退出程序。

线程在运行的生命周期当中可以变更其状态,但主线程默认为前台线程,使用 Thread 类型创建的线程默认也是前台线程。只有线程池的线程默认为后台线程,进入托管执行的本机代码创建的任何线程也会标记为后台线程。