线程的实现
# 线程的实现
# 线程的实现方式
线程已在许多系统中实现,但各系统的实现方式并不完全相同。在有的系统中,特别是一些数据库管理系统,如 infomix所实现的是用户级线程:而另一些系统(如 MacintoSh和OS/2操作系统)所实现的是内核支持线程;还有一些系统如 Solaris操作系统,则同时实现了这两种类型的线程。
- 内核支持线程KST( Kernel Supported Threads)
在OS中的所有进程,无论是系统进程还是用户进程,都是在操作系统内核的支持下运行的,是与内核紧密相关的。而内核支持线程KST同样也是在内核的支持下运行的,它们的创建、阻塞、撤消和切换等,也都是在内核空间实现的。为了对内核线程进行控制和管理,在内核空间也为每一个内核线程设置了一个线程控制块,内核根据该控制块而感知某线程的存在,并对其加以控制。当前大多数OS都支持内核支持线程。
这种线程实现方式主要有四个主要优点:
- 在多处理器系统中,内核能够同时调度同一进程中的多个线程并行执行
- 如果进程中的一个线程被阻塞了,内核可以调度该进程中的其它线程占有处理器运行,也可以运行其它进程中的线程;
- 内核支持线程具有很小的数据结构和堆栈,线程的切换比较快,切换开销小
- 内核本身也可以采用多线程技术,可以提高系统的执行速度和效率。
内核支持线程的主要缺点是:
- 对于用户的线程切换而言,其模式切换的开销较大,在同一个进程中,从一个线程切换到另一个线程时,需要从用户态转到核心态进行,这是因为用户进程的线程在用户态运行,而线程调度和管理是在内核实现的,系统开销较大.
- 用户级线程ULT( User Level Threads)
用户级线程是在用户空间中实现的。 对线程的创建、撤消、同步与通信等功能,都无需内核的支持,即用户级线程是与内核无关的。 在一个系统中的用户级线程的数目可以达到数百个至数千个。由于这些线程的任务控制块都是设置在用户空间,而线程所执行的操作也无需内核的帮助,因而内核完全不知道用户级线程的存在。
值得说明的是,对于设置了用户级线程的系统,其调度仍是以进程为单位进行的。在采用轮转调度算法时,各个进程轮流执行一个时间片,这对诸进程而言貌似是公平的假如在进程A中包含了一个用户级线程,而在另一个进程B中含有100个用户级线程,这样,进程A中线程的运行时间将是进程B中各线程运行时间的100倍:相应地,其速度要快上100倍,因此说实质上并不公平。
假如系统中设置的是内核支持线程,则调度便是以线程为单位进行的。在采用轮转法调度时,是各个线程轮流执行一个时间片。同样假定进程A中只有一个内核支持线程,而在进程B中有100个内核支持线程。此时进程B可以获得的CPU时间是进程A的100倍且进程B可使100个系统调用并发工作。
使用用户级线程方式有许多优点:
线程切换不需要转换到内核空间。对一个进程而言,其所有线程的管理数据结构均在该进程的用户空间中,管理线程切换的线程库也在用户地址空间运行,因此进程不必切换到内核方式来做线程管理,从而节省了模式切换的开销。
调度算法可以是进程专用的。在不干扰OS调度的情况下,不同的进程可以根据自身需要选择不同的调度算法,对自己的线程进行管理和调度,而与Os的低级调度算法是无关的。
用户级线程的实现与OS平台无关,因为对于线程管理的代码是属于用户程序的部分,所有的应用程序都可以对之进行共享。因此,用户级线程甚至可以在不支持线程机制的操作系统平台上实现。
而用户级线程方式的主要缺点则在于:
系统调用的阻塞问题。在基于进程机制的OS中,大多数系统调用将使进程阻塞,因此,当线程执行一个系统调用时,不仅该线程被阻塞,而且,进程内的所有线程会被阻塞。而在内核支持线程方式中,则进程中的其它线程仍然可以运行。
在单纯的用户级线程实现方式中,多线程应用不能利用多处理机进行多重处理的优点,内核每次分配给一个进程的仅有一个CPU,因此,进程中仅有一个线程能执行,在该线程放弃CPU之前,其它线程只能等待。
- 组合方式
有些OS把用户级线程和内核支持线程两种方式进行组合,提供了组合方式ULT/KST线程。在组合方式线程系统中,内核支持多个内核支持线程的建立、调度和管理,同时,也允许用户应用程序建立、调度和管理用户级线程。一些内核支持线程对应多个用户级线程,这是用户级线程通过时分多路复用内核支持线程来实现的。即将用户级线程对部分或全部内核支持线程进行多路复用,程序员可按应用需要和机器配置,对内核支持线程数目进行调整,以达到较好效果。组合方式线程中,同一个进程内的多个线程可以同时在多处理器上并行执行,而且在阻塞一个线程时并不需要将整个进程阻塞。所以,组合方式多线程机制能够结合KST和ULT两者的优点,并克服了其各自的不足。由于用户级线程和内核支持线程连接方式的不同,从而形成了三种不同的模型:
- 多对一模型
- 一对一模型
- 多对多模型
(1)多对一模型,即将用户线程映射到一个内核控制线程。如图2-18(a)所示,这些用户线程一般属于一个进程,运行在该进程的用户空间,对这些线程的调度和管理也是在该进程的用户空间中完成。仅当用户线程需要访问内核时,才将其映射到一个内核控制线程上,但每次只允许一个线程进行映射。该模型的主要优点是线程管理的开销小,效率高其主要缺点在于,如果一个线程在访问内核时发生阻塞,则整个进程都会被阻塞;此外,在任一时刻,只有一个线程能够访问内核,多个线程不能同时在多个处理机上运行。
(2)一对一模型,即将每一个用户级线程映射到一个内核支持线程。如图2-18(b)所示,为每一个用户线程都设置一个内核控制线程与之连接。该模型的主要优点是:当一个线程阻塞时,允许调度另一个线程运行,所以它提供了比多对一模型更好的并发功能。此外,在多处理机系统中,它允许多个线程并行地运行在多处理机系统上。该模型的唯一缺点是:每创建一个用户线程,相应地就需要创建一个内核线程,开销较大,因此需要限制整个系统的线程数。Windows 2000, Windows NT, OS/2等系统上都实现了该模型。
(3)多对多模型,即将许多用户线程映射到同样数量或更少数量的内核线程上。如图2-18(c)所示,内核控制线程的数目可以根据应用进程和系统的不同而变化,可以比用户线程少,也可以与之相同。该模型结合上述两种模型的优点,它可以像一对一模型那样,使个进程的多个线程并行地运行在多处理机系统上,也可像多对一模型那样,减少线程的管理开销和提高效率。
# 线程的实现
不论是进程还是线程,都必须直接或间接地取得内核的支持。由于内核支持线程可以直接利用系统调用为它服务,故线程的控制相当简单;而用户级线程必须借助于某种形式的中间系统的帮助方能取得内核的服务,故在对线程的控制上要稍复杂一些。
- 内核支持线程的实现
在仅设置了内核支持线程的OS中,一种可能的线程的控制方法是,系统在创建一个新进程时,便为它分配一个任务数据区PTDA(Per Task Data Area),其中包括若干个线程控制块TCB空间,如图2-19所示。在每一个TCB中可保存线程标识符、优先级、线程运行的CPU状态等信息。虽然这些信息与用户级线程TCB中的信息相同,但现在却是被保存在内核空间中。
每当进程要创建一个线程时,便为新线程分配一个TCB,将有关信息填入该TCB中,并为之分配必要的资源,如为线程分配数百至数千个字节的栈空间和局部存储区,于是新创建的线程便有条件立即执行。当PTDA中的所有TCB空间已用完,而进程又要创建新的线程时,只要其所创建的线程数目未超过系统的允许值(通常为数十至数百个),系统可再为之分配新的TCB空间;
在撤消一个线程时,也应回收该线程的所有资源和TCB.可见,内核支持线程的创建、撤消均与进程的相类似。 在有的系统中为了减少在创建和撤消一个线程时的开销,在撤消一个线程时并不立即回收该线程的资源和TCB,这样,当以后再要创建一个新线程时,便可直接利用已被撤消但仍保持源的тсв程的TCB.
内核支持线程的调度和切换与进程的调度和切换十分相似,也分抢占式方式和非抢占方式两种。在线程的调度算法上,同样可采用时间片轮转法、优先权算法等。当线程调度选中一个线程后,便将处理机分配给它。当然,线程在调度和切换上所花费的开销要比进程的小得多。
- 用户级线程的实现
用户级线程是在用户空间实现的。所有的用户级线程都具有相同的结构,它们都运行在一个中间系统上。当前有两种方式实现中间系统,即运行时系统和内核控制线程。
1)运行时系统(Runtime System)
所谓“运行时系统”,实质上是用于管理和控制线程的函数(过程)的集合,其中包括用于创建和撤消线程的函数、线程同步和通信的函数,以及实现线程调度的函数等。正因为有这些函数,才能使用户级线程与内核无关。运行时系统中的所有函数都驻留在用户空间,并作为用户级线程与内核之间的接口。
在传统的OS中,进程在切换时必须先由用户态转为核心态,再由核心来执行切换任务:而用户级线程在切换时则不须转入核心态,而是由运行时系统中的线程切换过程(函数),来执行切换任务,该过程将线程的CPU状态保存在该线程的堆栈中,然后按照一定的算法,选择一个处于就绪状态的新线程运行,将新线程堆栈中的CPU状态装入到CPU相应的寄存器中,一旦将栈指针和程序计数器切换后,便开始了新线程的运行。由于用户级线程的切换无须进入内核,且切换操作简单,因而使用户级线程的切换速度非常快。
不论在传统的OS中,还是在多线程OS中,系统资源都是由内核管理的。在传统的OS中,进程是利用OS提供的系统调用来请求系统资源的,系统调用通过软中断(如trp)机制进入OS内核,由内核来完成相应资源的分配。用户级线程是不能利用系统调用的。
当线程需要系统资源时,是将该要求传送给运行时系统,由后者通过相应的系统调用来获得系统资源。
2)内核控制线程
这种线程又称为轻型进程LWP(Light Weight Process)。每一个进程都可拥有多个LWP,同用户级线程一样,每个LWP都有自己的数据结构(如TCB),其中包括线程标识符、优先级、状态,另外还有栈和局部存储区等。LWP也可以共享进程所拥有的资源。LWP可通过系统调用来获得内核提供的服务,这样,当一个用户级线程运行时,只须将它连接到一个LWP上,此时它便具有了内核支持线程的所有属性。这种线程实现方式就是组合方式。
在一个系统中的用户级线程数量可能很大,为了节省系统开销,不可能设置太多的LWP,而是把这些LWP做成一个缓冲池,称为“线程池”。用户进程中的任一用户线程都可以连接到LWP池中的任何一个LWP上。为使每一用户级线程都能利用LWP与内核通信,可以使多个用户级线程多路复用一个LWP,但只有当前连接到LWP上的线程才能与内核通信,其余进程或者阻塞,或者等待LWP。而每一个LWP都要连接到一个内核级线程上,这样,通过LWP可把用户级线程与内核线程连接起来,用户级线程可通过LWP来访问内核,但内核所看到的总是多个LWP而看不到用户级线程。亦即,由LWP实现了内核与用户级线程之间的隔离,从而使用户级线程与内核无关。图2-20示出了利用轻型进程作为中间系统时用户级线程的实现方法。
当用户级线程不需要与内核通信时,并不需要LWP:而当要通信时,便须借助于LWP,而且每个要通信的用户级线程都需要一个LWP。例如,在一个任务中,如果同时有5个用户级线程发出了对文件的读、写请求,这就需要有5个LWP来予以帮助,即由LWP将对文件的读、写请求发送给相应的内核级线程,再由后者执行具体的读、写操作。如果一个任务中只有4个LWP,则只能有4个用户级线程的读、写请求被传送给内核线程,余下的一个用户级线程必须等待。
在内核级线程执行操作时,如果发生阻塞,则与之相连接的多个LWP也将随之阻塞,进而使连接到LWP上的用户级线程也被阻塞。如果进程中只包含了一个LWP,此时进程也应阻塞。这种情况与前述的传统OS一样,在进程执行系统调用时,该进程实际上是阻塞的。但如果在一个进程中含有多个LWP,则当一个LWP阻塞时,进程中的另一个LWP可继续执行:即使进程中的所有LWP全部阻塞,进程中的线程也仍然能继续执行,只是不能再去访问内核。
# 线程的创建和终止
如同进程一样,线程也是具有生命期的,它由创建而产生,由调度而执行,由终止而消亡。相应的,在OS中也就有用于创建线程的函数(或系统调用)和用于终止线程的函数(或系统调用)。
- 线程的创建应用程序在启动时,通常仅有一个线程在执行,人们把线程称为“初始化线程”,它的主要功能是用于创建新线程。在创建新线程时,需要利用一个线程创建函数(或系统调用),并提供相应的参数,如指向线程主程序的入口指针、堆栈的大小,以及用于调度的优先级等。在线程的创建函数执行完后,将返回一个线程标识符供以后使用。
- 线程的终止当一个线程完成了自己的任务(工作)后,或是线程在运行中出现异常情况而须被强行终止时,由终止线程通过调用相应的函数(或系统调用)对它执行终止操作。但有些线程(主要是系统线程),它们一旦被建立起来之后,便一直运行下去而不被终止。在大多数的OS中,线程被中止后并不立即释放它所占有的资源,只有当进程中的其它线程执行了分离函数后,被终止的线程才与资源分离,此时的资源才能被其它线程利用。
虽已被终止但尚未释放资源的线程仍可以被需要它的线程所调用,以使被终止线程重新恢复运行。为此,调用线程须调用一条被称为“等待线程终止”的连接命令来与该线程进行连接。如果在一个调用者线程调用“等待线程终止”的连接命令,试图与指定线程相连接时,若指定线程尚未被终止,则调用连接命令的线程将会阻塞,直至指定线程被终止后,才能实现它与调用者线程的连接并继续执行;若指定线程已被终止,则调用者线程不会被阻塞而是继续执行。