什么是线程?
能够将线程了解成逻辑 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();}
后果
结构 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 的线程池,线程池会主动治理线程的创立以及销毁,同时线程池创立的线程能够为各种工作重复使用,所以应用程序通常只须要几个线程便能够实现全副工作。