第四讲 线程

943 阅读9分钟

到目前为止提出的进程的概念包含两个特点:

  • 资源所有权:一个进程包括一个存放进程映像的虚拟地址空间;进程映像是程序、数据、栈和进程控制块中定义的属性的集合。一个进程总是拥有对资源的控制或所有权,这些资源包括内存、I/O通道、I/O设备和文件。操作系统执行保护功能,以防止进程之间发生不必要的与资源相关的冲突。
  • 调度/执行:一个进程沿着通过—个或多个程序的一条执行路径(轨迹)执行。其执行过程可能与其他进程的执行过程交替进行。因此,一个进程具有一个执行状态(运行、就绪等)和一个分配的优先级,并且是一个可被操作系统调度和分派的实体。

上述两个特点是独立的,操作系统能够独立地处理它们。为区分这两个特点,分派的单位通常称做线程或轻量级进程 (Light Weight Process, LWP),而拥有资源所有权的单位通常仍称做进程或任务。

多线程

多线程是指操作系统在单个进程内支持多个并发执行路径的能力。每个进程中只有一个线程在执行的传统方法(还没有明确线程的概念)称为单线程方法。我们关心的是使用多进程,且每个进程支持多个线程的情况。这一方法被 Windows、Solaris 和很多现代版本的 UNIx 等操作系统所来用。

1.png

在一个进程中,可能有一个或多个线程,每个线程有:

  • 线程执行状态(运行、就绪等)。
  • 在未运行时保存的线程上下文;从某种意义上看,线程可以被看做进程内的一个被独立地操作的程序计数器。
  • 一个执行栈。
  • 用于每个线程局部变量的静态存储空间。
  • 与进程内的其他线程共享的对进程的内存和资源的访问。

进程中的所有线程共享该进程的状态和资源,它们驻留在同一块地址空间中,并且可以访问到相同的数据。当一个线程改变了内存中的一个数据项时,其他线程在访问这一数据项时能够看到变化后的结果。如果一个线程以读权限打开一个文件,那么同一个进程中的其他线程也能够从这个文件中读取数据。 1.png

从性能比较可以看出线程的重要优点如下:

  • 在一个已有进程中创建一个新线程比创建一个全新进程所無的时间要少很多。Mach 开发者的研究表明,线程创建要比在 UNIX 中的进程创建快 10 倍ITEVA.871。
  • 终止一个线程比终止一个进程花费的时间少。
  • 同一进程内线程间切换比进程间切换花费的时间少。
  • 线程提高了不同的执行程序间通信的效率。在大多数操作系统中,独立进程间的通信需要内核的介入,以提供保护和通信所需要的机制。但是,由于在同一个进程中的线程共享内存和文件,它们无需调用内核就可以互相通信。

例子-文件服务器

使用线程的应用程序的一个例子是文件服务器。当每个新文件请求到达时,会为文件管理程序创建一个新的线程。由于服务器将会处理到很多请求,那么将会在短期内创建和销毁许多线程。

如果服务器运行在多处理器机器上,那么在同一个进程中的多个线程就可以同时在不同的处理器上执行。此外,由于文件服务中的进程或线程必须共享文件数据,并据此协调它们的行为.此时使用线程和共享内存比使用进程和消息传递要快。

线程的实现分为两大类: 用户线程内核线程

用户线程

在一个纯粹的用户级线程软件中,有关线程管理的所有工作都由应用程序完成,内核意识不到线程的存在。

1.png

任何应用程序都可以通过使用线程库,以便被设计成多线程程序。线程库是用于用户级线程管理的一个例程包,它包含用于创建和销毁线程的代码、在线程问传递消息和数据的代码、调度线程执行的代码以及保存和饮复线程上下文的代码。

在默认情况下,应用程序从单线程开始,并在该线程中开始运行。该应用程序和它的线程被分配给-一个由内核管理的进程。在应用程序正在运行(进程处于运行态)的任何时刻,应用程序都可以派生一个在相同进程中运行的新线程。派生线程是通过调用线程库中的派生例程完成的。

通过过程调用,控制权被传递给派生例程。线程库为新线程创建一个数据结构,然后使用某种调度算法,把控制权传递给该进程中处于就绪态的一个线程。当控制权被传递给线程库时,需要保存当前线程的上下文,然后 当控制权从线程库中传递给一个线程时,將恢复那个线程的上下文。

上下文实际上包括用户奇存器的内容、程序计数器和栈指针。

内核线程

在一个纯粹的内核级线程软件中,有关线程管理的所有工作都是由内核完成的,应用程序部分没有进行线程管理的代码,只有一个到内核线程设施的应用程序编程接口(API)。 Windows是这种方法的一个例子。

内核为进程及其内部的每个线程维护上下文信息。调度是由内核基于线程完成的。该方法克服了用户级线程方法的两个基本缺陷。首先,内核可以同时把同一个进程中的多个线程调度到多个处理器中;其次,如果进程中的一个线程被阻塞,内核可以调度同一个进程中的另一个线程。内核级线程方法的另一个优点是内核例程自身也是可以使用多线程的。

相对于用户级线程方法,内核级线程方法的主要缺点是:在把控制从一个线程传送到同一个进程内的另一个线程时,需要到内核的状态切换。

线程的优缺点

1.png

POSIX Pthreads线程库

为实现可移植的线程程序,IEEE在IEEE标准1003.1c中定义了线程的标准。它定义的线程包叫做Pthread。大部分UNIX系统都支持该标准。这个标准定义了超过60个函数调用,如果在这里列举一遍就太多了。取而代之的是,我们将仅仅描述一些主要的西数,以说明它是如何工作的。

1.png

所有Pthread线程都有某些特性。每一个都含有一个标识符、一组寄存器(包括程序计数器)和二组存储在结构中的属性。这些属性包括堆栈大小、调度参数以及使用线程需要的其他项目。

创建一个新线程需要使 用pthread_create调用。新创建的线程的线程标识符作为函数值返回。这种调用看起来很像fork系统调用,其中线程标识符起着PID的作用,而这么做的目的主要是为了标识在其他调用中引用的线程。

当一个线程完成分配给它的工作时,可以通过调用pthread_exi来终止。这个调用终止该线程并释放它的栈。

一般一个线程在继续运行前需要等待另一个线程完成它的工作并退出。可以通过pthread_join线程调用来等待别的特定线程的终止。而要等待线程的线程标识符作为一个参数给出。

有时会出现这种情况:一个线程逻辑上没有阻塞,但感觉上它已经运行了足够长时间并且希望给另外一个线程机会去运行。这时可以通过调用pthread_yield完成这一目标。而进程中没有这种调用,因为假设进程间会有激烈的竞争性,并且每一个进程都希望获得它所能得到的所有的CPU时间。但是,由于同一进程中的线程可以同时工作,并且它们的代码,总是由同一个程序员编写的,因此,有时程序员希望它们能互相给对方一些机会去运行。

下面两个线程调用是处理属性的。Pthread_attr_init建立关联一个线程的属性结构并初始化成默认值。

这些值(例如优先级)可以通过修改属性结构中的域值来改变。

最后,pthread_attr_destroy删除一个线程的属性结构,释放它占用的内存。它不会影响调用它的线程。这些线程会继续存在。

为了更好地了解Pthread是如何工作的,有个的简单例子。

1.png

这里主程序在宣布它的意图之后,循环NUMBER_OF_THREADS次,每次创建一个新的线程。如果线程创建失败,会打印出一条错误信息然后退出。在创建完所有线程之后,主程序退出。

当创建一个线程时,它打印一条一行的发布信息,然后退出。

这些不同信息交错的顺序是不确定的,并且可能在连续运行程序的情况下发生变化。

前面描述的Pthread调用还有许多的调用。这里会有进程与线程同步的新问题。

参考资料

第7章-进程 & 线程

C 中的多线程

Threads

POSIX Threads