1
什么是线程?

可以将线程理解成逻辑CPU,它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。

线程开销
  • 线程内核对象:OS为系统中创建的每个线程都会分配并初始化这种数据结构。在这个数据结构中,包含一组对线程进行描述的属性,还包含线程上下文。上下文是一个内存块,包含了CPU的寄存器集合。Windows在使用x 86CPU机器上运行时,线程上下文使用约700字节的内存,而对于x64 CPU上下文使用约1240字节内存。
  • 线程环境块:是在应用程序代码能快速访问的地址空间中分配和初始化的一个内存块。线程环境块耗用一个内存页(x86和x64都是4KB),线程环境块还包含线程的异常处理链首,线程进入的每个try块都在链首插入一个节点。线程退出try块时,会从链中删除该节点。除此之外线程环境块还包含线程本地存储数据以及GDI(图形设备接口)和OpenGL图形使用的一些数据结构。
  • 用户模式栈:用于存储传给方法的局部变量和实参。还包含一个地址,该地址指出当前方法返回时,线程接下去应该从什么地方开始执行,默认情况下Windows为每个用户模式栈分配1MB内存。
  • 内核模式栈:应用程序代码向操作系统中的一个内核模式的函数传递实参时,还会使用内核模式栈。出于安全考虑,针对从用户模式的代码传给内核的任何实参,Windows都会把它们从线程的用户模式栈中复制到线程的内核模式栈。复制后内核就可以验证实参的值。由于应用程序的代码无法访问内核模式栈,所以应用程序无法修改验证后的实参值。OS内核代码开始对复制的值进行处理。除此之外,内核还会调用它自己内部的方法,并利用内核模式栈传递它自己的实参、存储函数的局部变量和存储返回地址。在Windows32位上运行时,内核模式栈大小为12KB,在64位上运行时大小则为24KB。
  • DLL线程连接和线程分离通知:任何时候在进程中创建一个线程,都会调用那个进程中加载的所有DLL的DllMain方法,并传递一个DLL_THREAD_ATTACH标记。同理任何时刻终止一个线程都会调用进程中所有DLL的DllMain方法,并传递一个DLL_THREAD_DETACH标记。有的DLL需要利用这些通知,为进程中创建/销毁的每个线程执行一些特殊的初始化或者清理操作。
线程上下文切换

在任何给定时刻,Windows只将一个线程分配给一个CPU,线程允许运行一个“时间片”,一旦“时间片”到期,Windows就将上下文切换到另一个线程,每次上下文切换Windows都要执行以下操作:

  • 将CPU寄存器中的值保存到当前正在运行的线程的内核对象内部的一个上下文结构中
  • 从现有线程集合中选出一个线程供调度,如果这个线程属于另一个进程,Windows在开始执行任何代码或使用任何数据之前,还必须切换CPU的虚拟地址空间
  • 将所有上下文结构中的值加载到CPU寄存器中

上下文切换完成后,CPU执行所选的线程,直到它的“时间片”到期。然后会进行另一次上下文切换(大约每30msWindows会执行一次上下文切换)。上下文切换属于净开销,不具有任何性能上的收益。

如果应用程序的线程进入死循环,Windows将会定期抢占它,将一个不同的线程分配给一个实际的CPU,让新线程运行一会。假设新线程是“任务管理器”的线程,用户可以利用“任务管理器”终止包含死循环线程的进程。系统中的其它进程并不受影响扔可以继续运行,且不会丢失数据,也不必重启计算机,让用户拥有更好的体验。

Windows上下文切换到另一个线程时,会发生一定的性能损失。但是CPU现在是要执行一个不同的线程,而之前的线程的代码和数据还在CPU的高速缓存中,这让CPU不必经常访问RAM(访问RAM的速度比CPU高速缓存慢得多)。当Windows上下文切换到新线程时,新线程有可能要执行不同的代码并访问不同的数据,这些代码和数据不在CPU的高速缓存中,因此CPU必须访问RAM来填充它的高速缓存,以恢复高速运行的状态。但是30ms之后又会发生一次新的上下文切换。

*当一个时间片结束,再次调用同一个线程(而不是新的线程)时不会发生上下文切换,而会让线程继续运行,改善性能

垃圾回收性能与线程数量

执行垃圾回收时CLR会挂起所有线程,遍历它们的栈来查找根以便对堆中的对象进行标记,再次遍历栈,由于有的对象在压缩期间可能发生了移动, 需要更改新的根,再恢复所有线程。所以减少线程数量,可以提高垃圾回收的性能。

调试体验与线程数量

每次使用调试器并遇到一个断点,Windows都会挂起正在调试的应用程序中的所有线程,并在单步执行或者运行应用程序时恢复所有线程,因此线程越多,调试体验越差。

什么时候应该创建一个线程而不是使用线程池线程?
  • 线程需要以非普通线程优先级运行(所有线程池线程都以普通优先级运行)
  • 需要线程表现为一个前台线程,防止应用程序在线程结束它的任务之前终止(线程池线程始终是后台线程,如果终止进程它们可能无法完成任务)
  • 需要长时间运行一个任务,线程池为了判断是否需要创建一个额外的线程采用的逻辑较为复杂,直接创建则可以避开此问题
  • 要启动一个线程,并可以调用Thread的Abort方法提前终止
创建线程
private static void SomeMethod(object parameter)
{
    //方法由一个专用线程执行
    Console.WriteLine(parameter);
    Thread.Sleep(1000);

    //方法返回后专用线程将终止
}

static void Main()
{
    Thread thread1 = new Thread(SomeMethod);
    thread1.Start("start thread");

    //模拟做其它事情
    Thread.Sleep(10000);

    //等待线程终止
    thread1.Join();
    Console.WriteLine("按Enter键退出");
    Console.ReadKey();
}

结果
image.png

构造Thread对象是一个轻量级的操作,因为并没有实际上的创建一个操作系统线程。只有调用了Thread的Start方法才实际创建操作系统线程并让它开始执行回调方法。调用Thread对象的Join方法会使调用线程阻塞当前执行的任何代码,直到创建的线程(thread1)线程终止或销毁。

使用线程有什么好处?
  • 隔离代码,提高应用程序的可靠性
  • 简化代码
  • 实现并发执行
线程调度和优先级

Windows属于抢占式多线程操作系统,所谓的抢占式是指线程可以在任何时间停止(被抢占),并调度另一个线程。

线程的优先级从低到高为0-31,首先检查优先级为31的线程,并以轮流的方式调度它们。如果线程可以调度,就把它分配给一个CPU,在这个线程的时间片结束时,系统会检查是够存在另一个优先级为31的线程可以运行,如果是就将该线程分配给一个CPU。

只要存在优先级31的线程,系统就永远不会将优先级0-31的线程分配给CPU,出现较高优先级线程总是占用CPU的时间,导致较低优先级的线程始终无法运行的现象,该现象称为饥饿(在多处理器的机器上出现饥饿的情况很小)。

较高优先级的线程总是会抢占较低优先级的线程,无论正在运行的属于何种较低优先级的线程。如:有一个优先级为1的线程正在运行,现在系统确定有一个优先级为5的线程已经准备好运行了,系统会立即挂起(暂停)优先级为1的线程,将CPU分配给优先级为5的线程,该线程拥有一个完整的时间片

系统启动时,会创建整个系统中唯一的一个优先级为0的线程,称之为零页线程。这个线程负责在没有其它进程需要执行的时候,将系统RAM的所有空闲页清零

*高优先级线程在其生命周期中的大多数时间都应处于等待状态,这样才不会影响系统的总体响应能力

前台线程和后台线程

CLR将每个线程要么视为前台线程,要么视为后台线程。一个进程中的所有前台线程停止运行时,CLR将强制终止仍然在运行的所有后台线程。这些后台线程将直接终止且不会抛出异常。

前台线程和后台线程之间的差异

private static void SomeMethod()
{
    Thread.Sleep(10000);

    //只有被前台线程执行时才会显示出来
    Console.WriteLine("Something Done");
}

static void Main()
{
    //创建一个新的线程,默认为前台线程
    Thread t = new Thread(SomeMethod);

    //改变前台线程为后台线程
    t.IsBackground = true;

    //启动线程
    t.Start();

    //如果t是前台线程,应用程序10s后终止,如果是后台线程,应用程序立即终止
    Console.WriteLine("回到主线程");
}

结果:应用窗口一闪而过,且SomeMethod方法中的输出内容也没有显示

总结:
在一个线程的生存期中,任何时候都可以从前后台线程互相切换。应用程序的主线程和通过构造Thread对象显示创建的任何线程默认都是前台线程。而线程池线程默认都是后台线程。我们要尽量避免使用前台线程,而使用CLR的线程池,线程池会自动管理线程的创建以及销毁,同时线程池创建的线程可以为各种任务重复使用,所以应用程序通常只需要几个线程便可以完成全部工作。


DoubleJ
7 声望3 粉丝

下一篇 »
线程Ⅱ