线程详细剖析(一)
摘自《C++多核高级编程》
6.1 什么时线程
线程时进程中可执行代码流的序列,它被操作系统调度,并在处理器或内核上运行。所有的进程都有一个主线程(primary thread)。主线程是进程的控制流或执行线路。具有多个线程的进程拥有和线程数目一样多的控制流。每个线程独立并发的执行自身的指令序列。具有多个线程的进程时多线程的。线程分为用户级线程和内核级线程。与进程相比,内核级线程在创建、维护和管理方面给操作系统带来的负担都要轻的多,因为与线程关联的信息很少。内核线程被称作轻量级进程,因为它的开销要比进程少。
线程执行程序中无关的并发任务。线程可用于简化具有固有并发的应用程序的程序结构,其方式与通过函数或过程来封装功能性使得应用程序的结构更简单相同。线程可以封装并发功能。线程在一个进程的地址空间中使用最少的共享资源,相比之下,应用程序则使用多个进程。这使得操作系统得到总体更加简单的程序结构。如果正确使用,线程可以通过利用多核处理器并发来改进应用程序的吞吐率和性能。每个线程负责被分配的一个子任务,然后线程独立管理子任务的执行。可以为每个线程指定反映它执行的子任务的重要性的优先级。
6.1.1 用户级线程和内核级线程
线程有三种实现模型:
1)用户级或应用程序级线程
2)内核级线程
3)用户级和内核级混合线程
图6-1显示了3种线程实现模型。图6-1(a)显示了用户级线程,(b)显示了内核级线程,(C)显示了用户级线程和内核级线程的混合。
这些实现之间较大的区别之一就是它们的模式以及要指派给处理器的线程的能力。这些线程运行在用户模式下或内核模式下。
1)在用户模式下,进程或线程时执行程序或链接库中的执行,它们不对操作系统内核进行任何调用。
2)在内核模式下,进程或线程时进行系统调用,例如访问资源或抛出异常。同时,在内核模式下,进程或线程可以访问在内核空间中定义的对象。
用户级线程驻留在用户空间或模式。运行时库管理这些线程,它也位于用户空间。它们对于操作系统是不可见的,因此无法被调度到处理器内核。每个线程并不具有自身的线程上下文。因此,就线程的同时执行而言,任意给定时刻每个进程并不具有一个线程在运行,而且只有一个处理器内核会被分配给该进程。对于一个进程,可能有成千上万个用户级线程,但是它们对系统资源没有影响。运行时库调度并分派这些线程。如同在图6-1(a)中看到的那样,库调度器从进程的多个线程中选择一个线程,然后该线程和该进程运行的一个内核线程关联起来。内核线程将被操作系统调度器指派到处理器内核。用户级线程时一种“多对一”的线程映射。
内核级线程驻留在内核空间,它们是内核对象。有了内核线程,每个用户线程被映射或绑定到一个内核线程。用户线程在其生命期内都会绑定到该内核线程。一旦用户线程终止,两个线程豆浆离开系统。这被称作“一对一”线程映射,如图6-1(b)所示。操作系统调度器管理、调度并分派这些线程。运行时库为每个用户级线程请求一个内核级线程。操作系统的内存管理和调度子系统必须要考虑到数量巨大的用户级线程。您必须了解每个进程允许的线程的最大数目是多少。操作系统为每个线程创建上下文。线程的上下文将在本章稍后部分介绍。进程的每个线程在资源可用时都可以被分派到处理器内核。
混合线程实现时用户线程和内核线程的交叉,使得库和操作系统都可以管理线程。用户线程由运行时库调度器管理,内核线程有操作系统调度器管理。在这种实现中,进程有着自己内核线程池。可运行的用户线程由运行时库分派并标记为准备号执行的可用线程。操作系统选择用户线程并将它映射到线程池中的可用内核线程。多个用户线程可以分配给相同的内核线程。在图6-1(c)中,进程A在它的线程池中有两个内核线程,而进程B有3个内核线程。进程A的用户线程2和3被映射到内核线程(2),进程B有5个线程,用户线程1和2映射到同一个内核线程(3),用户线程4好5映射到内核同一个内核线程(5)。当创建新的用户线程时,只需要简单的将它映射到线程池中现有的一个内核线程即可。这种实现使用了“多对多”线程映射。该方法中尽量使用多对一映射。很多用户线程将会映射到一个内核线程,就像您在前面的实例中看到的。因此,对内核线程的轻轻将会少于用户线程的数目。
内核线程池不会被销毁和重建,这些线程总是位于系统中。它们会在必要时分配给不同的用户级线程,而不是当创建新的用户级线程时就创建一个新的内核线程,而纯内核级线程被创建时,就会创建一个新的内核线程。只对池中的每个线程创建上下文。有了内核线程和混合线程,操作系统分配一组处理器内核,进程的线程可以在这些处理器内核之上运行。线程只能在为它们所属线程指派的处理器内核上运行。
在确定线程的调度模型和竞争范围时,用户级线程和内核级线程变得很重要。竞争范围决定了指定的线程与那些线程竞争处理器的使用,而且对于操作系统对大量线程的内存管理也非常重要。