线程的基本概念

如果说,在操作系统中引入进程的目的,是为了使多个程序能并发执行,以提高资源利用率和系统吞吐量,那么,在操作系统中再引入线程,则是为了减少程序在并发执行时所付出的时空开销,使 OS 具有更好的并发性。

Posted by Silence on June 20, 2017

线程的引入

如果说,在操作系统中引入进程的目的,是为了使多个程序能并发执行,以提高资源利用率和系统吞吐量,那么,在操作系统中再引入线程,则是为了减少程序在并发执行时所付出的时空开销,使 OS 具有更好的并发性。

由于进程是一个资源的拥有者,因而在创建、撤消和切换中,系统必须为之付出较大的时空开销。线程作为调度和分派的基本单位。

线程与进程的比较

线程具有许多传统进程所具有的特征,所以又称为轻型进程(Light-Weight Process)或进程元,相应地把传统进程称为重型进程(Heavy-WeightProcess)

  • 调度

在传统的操作系统中,作为拥有资源的基本单位和独立调度、分派的基本单位都是进程。而在引入线程的操作系统中,则把线程作为调度和分派的基本单位,而进程作为资源拥有的基本单位,使线程基本上不拥有资源,这样线程便能轻装前进,从而可显著地提高系统的并发程度。在同一进程中,线程的切换不会引起进程的切换,但从一个进程中的线程切换到另一个进程中的线程时,将会引起进程的切换。

  • 并发性

    在引入线程的操作系统中,不仅进程之间可以并发执行,而且在一个进程中的多个线程之间亦可并发执行,使得操作系统具有更好的并发性,从而能更加有效地提高系统资源的利用率和系统的吞吐量

  • 拥有资源

    进程是系统中拥有资源的一个基本单位。一般而言,线程自己不拥有系统资源(也有一点必不可少的资源),但它可以访问其隶属进程的资源,即一个进程的代码段、数据段及所拥有的系统资源,如已打开的文件、I/O设备等,可以供该进程中的所有线程所共享。

  • 系统开销

    在创建或撤消进程时,系统都要为之创建和回收进程控制块,分配或回收资源,如内存空间和I/O设备等,操作系统所付出的开销明显大于线程创建或撤消时的开销。类似地,在进程切换时,涉及到当前进程CPU环境的保存及新被调度运行进程的CPU环境的设置,而线程的切换则仅需保存和设置少量寄存器内容,不涉及存储器管理方面的操作,所以就切换代价而言,进程也是远高于线程的。此外,由于一个进程中的多个线程具有相同的地址空间,在同步和通信的实现方面线程也比进程容易。在一些操作系统中,线程的切换、同步和通信都无须操作系统内核的干预。

线程的属性

  • 轻型实体

    线程中的实体基本上不拥有系统资源,只是有一点必不可少的、能保证其独立运行的资源,比如,在每个线程中都应具有一个用于控制线程运行的线程控制块 TCB,用于指示被执行指令序列的程序计数器,保留局部变量、少数状态参数和返回地址等的一组寄存器和堆栈。

  • 独立调度和分派的基本单位

  • 可并发执行

  • 共享进程资源

线程的状态

  • 状态参数

    在 OS 中的每一个线程都可以利用线程标识符和一组状态参数进行描述。状态参数通常有这样几项:

    • 寄存器状态,它包括程序计数器 PC(存放下一条指令所在单元) 和堆栈指针中的内容;

    • 堆栈,在堆栈中通常保存有局部变量和返回地址;

    • 线程运行状态,用于描述线程正处于何种运行状态;

    • 优先级,描述线程执行的优先程度;

    • 线程专有存储器,用于保存线程自己的局部变量拷贝;

    • 信号屏蔽,即对某些信号加以屏蔽。

  • 线程运行状态

    • 执行状态

      表示线程正获得处理机而运行;

    • 就绪状态

      指线程已具备了各种执行条件,一旦获得 CPU 便可执行的状态;

    • 阻塞状态

      指线程在执行中因某事件而受阻,处于暂停执行时的状态。

线程的创建和终止

在多线程 OS 环境下,应用程序在启动时,通常仅有一个“初始化线程”在执行。它可根据需要再去创建若干个线程。

  • 创建新线程

    • 调用线程创建函数
    • 提供相应的参数,如指向线程主程序的入口指针、堆栈的大小,以及用于调度的优先级等
    • 返回一个线程标识符
  • 终止线程

    • 在线程完成了自己的工作后自愿退出
    • 在运行中出现错误或由于某种原因而被其它线程强行终止
    • 线程被中止后并不立即释放它所占有的资源,只有当进程中的其它线程执行了 分离函数 后,被终止的线程才与资源分离,此时的资源才能被其它线程利用

    • 虽已被终止但尚未释放资源的线程,仍可以被需要它的线程所调用,以使被终止线程重新恢复运行。为此,调用者线程须调用一条被称为“等待线程终止”的连接命令,来与该线程进行连接。如果在一个调用者线程调用“等待线程终止”的连接命令试图与指定线程相连接时,若指定线程尚未被终止,则调用连接命令的线程将会阻塞,直至指定线程被终止后才能实现它与调用者线程的连接并继续执行;若指定线程已被终止,则调用者线程不会被阻塞而是继续执行。

线程间的同步和通信

  • 互斥锁(mutex)

    互斥锁是一种比较简单的、用于实现线程间对资源互斥访问的机制。由于操作互斥锁的时间和空间开销都较低,因而较适合于高频度使用的关键共享数据和程序段。互斥锁可以有两种状态,即开锁(unlock)和关锁(lock)状态。相应地,可用两条命令(函数)对互斥锁进行操作。其中的关锁 lock 操作用于将 mutex 关上,开锁操作 unlock 则用于打开 mutex。

  • 条件变量

    线程首先对 mutex执行关锁操作,若成功便进入临界区,然后查找用于描述该资源状态的数据结构,以了解资源的情况。只要发现所需资源R正处于忙碌状态,线程便转为等待状态,并对mutex执行开锁操作后 ,等待该资源被释放;若资源处于空闲状态,表明线程可以使用该资源,于是 将该资源设置为忙碌状态,再对 mutex执行开锁操作。下面给出了对上述资源的申请(左半部分)和释放(右半部分)操作的描述。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    
      Lock mutex
          check data structures;
          if (resource busy)
              wait(condition variable);
          mark resource as busy;
      unlock mutex;
    
      Lock mutex
      mark resource as free;
      unlock mutex;
      wakeup(condition variable);
    

    原来占有资源 R 的线程在使用完该资源后,便按照右半部分的描述释放该资源,其中的 wakeup(conditionvariable)表示去唤醒在指定条件变量上等待的一个或多个线程。在大多数情况下,由于所释放的是临界资源,此时所唤醒的只能是在条件变量上等待的某一个线程,其它线程仍继续在该队列上等待。但如果线程所释放的是一个数据文件,该文件允许多个线程同时对它执行读操作。在这种情况下,当一个写线程完成写操作并释放该文件后,如果此时在该条件变量上还有多个读线程在等待,则该线程可以唤醒所有的等待线程。

信号量机制

  • 私用信号量

    当某线程需利用信号量来实现同一进程中各线程之间的同步时,可调用创建信号量的命令来创建一私用信号量,其数据结构存放在应用程序的地址空间中。私用信号量属于特定的进程所有,OS并不知道私用信号量的存在,因此,一旦发生私用信号量的占用者异常结束或正常结束,但并未释放该信号量所占有空间的情况时,系统将无法使它恢复为0(空),也不能将它传送给下一个请求它的线程。

  • 公用信号量

    公用信号量是为实现不同进程间或不同进程中各线程之间的同步而设置的。由于它有着一个公开的名字供所有的进程使用,故而把它称为公用信号量。其数据结构是存放在受保护的系统存储区中,由OS为它分配空间并进行管理,故也称为系统信号量。如果信号量的占有者在结束时未释放该公用信号量,则 OS 会自动将该信号量空间回收,并通知下一进程。

线程的实现方式

  • 内核支持线程

    无论是用户进程中的线程,还是系统进程中的线程,他们的创建、撤消和切换等也是依靠内核,在内核空间实现的。此外,在内核空间还为每一个内核支持线程设置了一个线程控制块,内核是根据该控制块而感知某线程的存在,并对其加以控制。

    这种线程实现方式主要有如下四个优点:

    • 在多处理器系统中,内核能够同时调度同一进程中多个线程并行执行;

    • 如果进程中的一个线程被阻塞了,内核可以调度该进程中的其它线程占有处理器运行,也可以运行其它进程中的线程;

    • 内核支持线程具有很小的数据结构和堆栈,线程的切换比较快,切换开销小;

    • 内核本身也可以采用多线程技术,可以提高系统的执行速度和效率。

    内核支持线程的主要缺点是:

    • 对于用户的线程切换而言,其模式切换的开销较大,在同一个进程中,从一个线程切换到另一个线程时,需要从用户态转到内核态进行,这是因为用户进程的线程在用户态运行,而线程调度和管理是在内核实现的,系统开销较大
  • 用户级线程

    用户级线程 ULT(User Level Threads)仅存在于用户空间中。对于这种线程的创建、撤消、线程之间的同步与通信等功能,都无须利用系统调用来实现。对于用户级线程的切换,通常发生在一个应用进程的诸多线程之间,这时,也同样无须内核的支持。由于切换的规则远比进程调度和切换的规则简单,因而使线程的切换速度特别快。

线程的实现

  • 内核支持线程的实现

    在仅设置了内核支持线程的OS中,一种可能的线程控制方法是,系统在创建一个新进程时,便为它分配一个任务数据区 PTDA(Per Task Data Area),其中包括若干个线程控制块TCB 空间。在每一个 TCB 中可保存线程标识符、优先级、线程运行的CPU状态等信息。虽然这些信息与用户级线程 TCB 中的信息相同,但现在却是被保存在内核空间中。

    每当进程要创建一个线程时,便为新线程分配一个TCB,将有关信息填入该 TCB 中,并为之分配必要的资源,如为线程分配数百至数千个字节的栈空间和局部存储区,于是新创建的线程便有条件立即执行。当 PTDA中的所有 TCB 空间已用完,而进程又要创建新的线程时,只要其所创建的线程数目未超过系统的允许值(通常为数十至数百个),系统可再为之分配新的 TCB 空间;在撤消一个线程时,也应回收该线程的所有资源和TCB。可见,内核支持线程的创建、 撤消均与进程的相类似。在有的系统中为了减少创建和撤消一个线程时的开销,在撤消一个线程时,并不立即回收该线程的资源和 TCB,当以后再要创建一个新线程时,便可直接利用已被撤消但仍保持有资源和 TCB 的线程作为新线程。

  • 用户级线程的实现

    用户级线程是在用户空间实现的。所有的用户级线程都具有相同的结构,它们都运行在一个中间系统的上面。当前有两种方式实现的中间系统,即运行时系统和内核控制线程。

    • 运行时系统(Runtime System)

      所谓“运行时系统”,实质上是用于管理和控制线程的函数(过程)的集合,其中包括用于创建和撤消线程的函数、线程同步和通信的函数以及实现线程调度的函数等。正因为有这些函数,才能使用户级线程与内核无关。运行时系统中的所有函数都驻留在用户空间,并作为用户级线程与内核之间的接口。

      在传统的 OS 中,进程在切换时必须先由用户态转为核心态,再由核心来执行切换任务;而用户级线程在切换时则不需转入核心态,而是由运行时系统中的线程切换过程来执行切换任务。该过程将线程的CPU状态保存在该线程的堆栈中,然后按照一定的算法选择一个处于就绪状态的新线程运行,将新线程堆栈中的 CPU 状态装入到CPU相应的寄存器中,一旦将栈指针和程序计数器切换后,便开始了新线程的运行。由于用户级线程的切换无需进入内核,且切换操作简单,因而使用户级线程的切换速度非常快。

      不论在传统的 OS 中,还是在多线程OS中,系统资源都是由内核管理的。在传统的OS 中,进程是利用OS提供的系统调用来请求系统资源的,系统调用通过软中断(如 trap)机制进入OS内核,由内核来完成相应资源的分配。用户级线程是不能利用系统调用的。当线程需要系统资源时,是将该要求传送给运行时系统,由后者通过相应的系统调用来获得系统资源的。

    • 内核控制线程

      这种线程又称为轻型进程 LWP(Light Weight Process)。每一个进程都可拥有多个 LWP,同用户级线程一样,每个 LWP 都有自己的数据结构(如TCB),其中包括线程标识符、优先级、状态,另外还有栈和局部存储区等。它们也可以共享进程所拥有的资源。LWP可通过系统调用来获得内核提供的服务,这样,当一个用户级线程运行时,只要将它连接到一个LWP 上,此时它便具有了内核支持线程的所有属性。这种线程实现方式就是组合方式。

      在一个系统中的用户级线程数量可能很大,为了节省系统开销,不可能设置太多的LWP,而把这些LWP做成一个缓冲池,称为“线程池”。用户进程中的任一用户线程都可以连接到 LWP 池中的任何一个 LWP 上。为使每一用户级线程都能利用LWP与内核通信,可以使多个用户级线程多路复用一个 LWP,但只有当前连接到LWP上的线程才能与内核通信,其余进程或者阻塞,或者等待 LWP。而每一个 LWP 都要连接到一个内核级线程上,这 样,通过 LWP 可把用户级线程与内核线程连接起来,用户级线程可通过 LWP 来访问内核,但内核所看到的总是多个 LWP 而看不到用户级线程。亦即,由 LWP 实现了在内核与用户级线程之间的隔离,从而使用户级线程与内核无关。