线程的相关知识

线程

线程的使用

为什么要在进程的基础上再创建一个线程的概念?

  • 多线程之间会共享同一块地址空间和所有可用数据的能力,这是进程所不具备的。
  • 线程比进程更加轻量级,所以它们比进程更容易(即更快)创建,也更容易撤销。在许多系统中,创建一个线程比创建一个进程要快10~100倍。
  • 第三个原因涉及性能方面的讨论。若多个线程都是CPU密集型的,那么并不能获得性能上的增强,但是如果存在着大量的计算和大量的I/O处理,拥有多个线程允许这些活动彼此重叠进行,从而会加快应用程序执行的速度。

多线程解决方案

一个万维网服务器。对页面的请求发给服务器,而所请求的页面发回给客户机。在多数Web站点上,某些页面较其他页面相比,有更多的访问。利用这一事实,Web服务器都可以把获得大量访问的页面集合保存在内存中,避免到磁盘去调入这些页面,从而改善性能。这样的一种页面集合称为高速缓存
在这里插入图片描述
所示,一种组织Web服务器的方式如图2-8所示。在这里,一个称为分派程序的线程从网络中读入工作请求。在检查请求之后,分派线程挑选一个空转的(即被阻塞的)工作线程,提交该请求,通常是在每个线程所配有的某个专门字中写入一个消息指针。接着分派线程唤醒睡眠的工作线程,将它从阻塞状态转为就绪状态。

在工作线程被唤醒之后,它检查有关的请求是否在Web页面高速缓存之中,这个高速缓存是所有线程都可以访问的。如果没有,该线程开始一个从磁盘调入页面的read操作,并且阻塞直到该磁盘操作完成。当上述线程阻塞在磁盘操作上时,为了完成更多的工作,分派线程可能挑选另一个线程运行,也可能把另一个当前就绪的工作线程投入使用。

这种模型允许把服务器编写为顺序线程的一个集合。在分派线程的程序中包含一个无限循环,该循环用来获得请求并且把工作请求派给工作线程。每个工作线程的代码包含一个从分派线程接受的请求,并且检查Web高速缓存中是否存在所需页面的无限循环。如果存在,就将该页面返回给客户机,接着该工作线程阻塞,等待一个新的请求。如果没有,工作线程就从磁盘调入该页面,将该页面返回给客户机,然后该工作线程阻塞,等待一个新的请求。

图2-9给出了有关代码的大致框架。如同本书的其他部分一样,这个假设TRUE为常数1。另外,bufpage分别是保存工作请求和Web页面的相应结构。

在这里插入图片描述

单线程解决方案

现在没有考虑多线程的情况下,如何编写Web服务器。一种可能的方式是,使其像一个线程一样运行。Web服务器的主循环获得请求,检查请求,并且在取下一个请求之前完成整个工作。在等待磁盘操作时,服务器就空转,并且不处理任何到来的其他请求。如果该Web服务器运行在唯一的机器上,通常情形都是这样,那么在等待磁盘操作时CPU只能空转。结果导致每秒钟只有很少的请求被处理。可见线程较好地改善了Web服务器的性能,并且每个线程都是按通常方式顺序编程的。

状态机解决方案

到目前为止,我们已经有了两种解决方案,单线程解决方案和多线程解决方案,其实还有一种解决方案就是状态机解决方案。

它的流程如下:

如果目前只有一个非阻塞版本的read系统调用可以使用,那么当请求到达服务器时,这个唯一的read调用的线程会进行检查,如果能够从高速缓存中得到相应,那么直接返回,如果不能,则启动一个非阻塞的磁盘操作。

服务器在表中记录当前请求的状态,然后进入并获取下一事件,紧接着下一个事件可能就是一个新工作的请求或是磁盘对先前操作的回答。如果是新工作的请求,那么就开始处理请求。如果是磁盘的响应,就从表中取出对应的状态信息进行处理。对于非阻塞式I/O而言,这种响应一般都是信号中断响应。

每次服务器从某个请求工作的状态切换到另一个状态时,都必须显式地保存或重新装入的计算状态。这里,每个计算机都有一个被保存的状态,存在一个会发生且使得相关状态发生改变的事件集合,我们把这类涉及称为有限状态机

模型特性
单线程无并行性,性能较差,阻塞系统调用
多线程有并行性,阻塞系统调用
有限状态机并行性,非阻塞系统调用,中断

经典的线程模型

理解进程的一个角度是,用某种方法把相关的资源集中在一起。进程有存放程序正文和数据以及其他资源的地址空间。这些资源中包括打开的文件,子进程,即将发生的定时器,信号处理程序,账号信息等。把它们都放到进程中可以更容易管理。

另一个概念是,进程拥有一个执行的线程,通常简写为线程。在线程中有一个程序计数器,用来记录接着要执行哪一条指令。线程拥有寄存器,用来保存线程当前的工作变量。线程还拥有一个栈,用来记录执行历史

尽管线程必须在某个进程中执行,但是线程和它的进程是不同的概念,并且可以分别处理。进程用于把资源集中到一起,而线程则是在CPU上被调度执行的实体

线程给进程模型增加了一项内容,即在同一个进程环境中,允许彼此之间有较大独立性的多个线程执行。在同一个进程中并行运行多个线程,是对在同一台计算机上并行运行多个进程的模拟。在前一种情形下,多个线程共享同一个地址空间和其他资源。而在后一种情形中,多个进程共享物理内存,磁盘,打印机和其他资源。由于线程具有进程的某些性质,所以有时被称为轻量级进程

多线程这个术语,也用来描述在同一个进程中允许多个线程的情形。

在图2-11a中,可以看到三个传统的进程。每个进程有自己的地址空间和单个控制线程。相反,在2-11b中,可以看到一个进程带有三个控制线程。尽管在两种情形中,都有三个线程。但是在图2-11a中,每一个线程都在不同的地址空间中运行,而在图2-11b中,这三个线程全部在相同的地址空间中运行。

在这里插入图片描述

进程中的不同线程不像不同进程之间那样存在很大的独立性。所有的线程都有完全一样的地址空间,这也意味着它们也共享同样的全局变量。由于各个线程都可以访问进程地址空间中的每一个内存地址,所以一个线程可以读,写或甚至清除另一个线程的堆栈

线程之间是没有保护的,原因是(1)不可能(2)没有必要。这与不同进程是有差别的。不同的进程会来自不同的用户,它们彼此之间可能有敌意,一个进程总是由某个用户所拥有,该用户创建多个线程应该是为了它们之间的合作而不是彼此间争斗。除了共享地址空间之外,所有线程还共享同一个打开文件集,子进程,定时器以及相关信号等

这样,对于三个没有关系的线程而言,应该使用图2-11a的结构,而在三个线程实际完成同一个作业,并彼此积极密切合作的情形中,图2-11b比较合适。

每个进程中的内容每个线程中的内容
地址空间程序计数器
全局变量寄存器
打开文件堆栈
子进程状态
即将发生的定时器
信号与信号处理程序
账户信息

第一列给出了在一个进程中所有线程共享的内容,第二列给出了每个线程自己的内容。第一列是进程的属性,而不是线程的属性。例如,如果一个线程打开了一个文件,该文件对该进程中的其他线程都是可见的。这些线程可以对该文件进行读写。

线程概念试图实现的是,共享一组资源的多个线程的执行能力,以便这些线程可以为完成某一任务而共同工作

和传统进程(即只有一个线程的进程)一样,线程可以处于若干种状态的任何一个:运行,阻塞,就绪或终止。正在运行的线程拥有CPU并且是活跃的。被阻塞的线程正在等待某个释放它的实践。线程可以被阻塞,以便等待某个外部事件的发生或者等待其他线程来释放它。就绪线程可被调度运行,并且轮到它就可以运行。线程状态之间的转换和进程状态之间的转换是一样的

每个线程有自己的堆栈。每个线程的堆栈有一帧,供各个被调用但是还没有从中返回的过程使用。在该栈帧中存放了相应过程的局部变量以及过程调用完成之后使用的返回地址。
在这里插入图片描述

线程系统调用

在多线程的情况下,进程通常会从当前的单个线程开始。这个线程有能力通过调用一个库函数thread_create创建新的线程。thread_create的参数专门指定了新线程要运行的过程名。这里,没有必要对新线程的地址空间加以锁定,因为新线程会自动在创建线程的地址空间中运行。有时,线程是有层次的,它们具有一种父子关系。但是,通常不存在这样一种关系。所有的线程都是平等的。不论有无层次关系,创建线程通常都返回一个线程标识符,该标识符就是新线程的名字

当一个线程完成工作后,可以通过调用一个库过程thread_exit退出。该线程接着消失,不再可调度。在某些线程系统中,通过调用一个过程,例如thread_join,一个线程可以等待另一个特定线程退出。这个过程阻塞调用线程直到特定线程退出。在这种情况下,线程的创建和终止非常类似于进程的创建和终止。

另一个常见的线程调用是thread_yield,它允许线程自动放弃CPU从而让另一个线程运行。这样一个调用是很重要的,因为不同于进程,线程无法利用时钟中断强制线程让出CPU。

POSIX线程

为了编写可移植线程程序,IEEE定义了线程的标准。它定义的线程包叫做pthread。大部分UNIX系统都支持该标准。

POSIX线程(通常称为pthreads)是一种独立于语言而存在的执行模型,以及并行执行模型。它允许程序控制时间上重叠的多个不同的工作流程。每个工作流程都称为一个线程,可以通过调用POSIX Thread API来实现这些流程的创建和控制。可以把它理解为线程的标准。

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

一些pthread的函数调用。

线程调用描述
pthread_create创建一个线程
pthread_exit结束调用的线程
pthread_join等待一个特定的线程退出
pthread_yield释放CPU来运行另外一个线程
pthread_attr_init创建并初始化一个线程的属性结构
pthread_attr_destroy删除一个线程的属性结构

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

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

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

有时候会出现这种情况:一个线程逻辑上没有阻塞,但感觉上它已经运行了足够长事件并且希望给另外一个线程机会去运行。这是可以通过调用pthread_yield完成这一目标。

pthread_attr_init建立关联一个线程的属性结构并初始化成默认值。这些值(例如优先级)可以通过修改属性结构中的域值来改变。

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

为了更好的了解pthread是如何工作的。考虑下面的例子。这里主程序在宣布它的意图之后,循环NUMBER_OF_THREADS次,每次创建一个新的线程。如果线程创建失败,会打印出一条错误信息然后退出。

#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>

#define NUMBER_OF_THREADS 10

void *print_hello_world(vvoid *tid){
  /* 输出线程的标识符,然后退出 */
  printf("Hello World. Greetings from thread %d\n",tid);
  pthread_exit(NULL);
}

int main(int argc,char *argv[]){
  /* 主程序创建 10 个线程,然后退出 */
  pthread_t threads[NUMBER_OF_THREADS];
  int status,i;
 	
  for(int i = 0;i < NUMBER_OF_THREADS;i++){
    printf("Main here. Creating thread %d\n",i);
    status = pthread_create(&threads[i], NULL, print_hello_world, (void *)i);
    
    if(status != 0){
      printf("Oops. pthread_create returned error code %d\n",status);
      exit(-1);
    }
  }
  exit(NULL);
}

线程实现

主要有三种实现方式

  • 在用户空间中实现线程。
  • 在内核空间中实现线程。
  • 在用户和内核空间中混合实现线程。

在用户空间中实现线程

把整个线程包放在用户空间中,内核对线程包一无所知。从内核角度考虑,就是按正常的方式管理,即单线程进程。这种方法第一个也是最明显的优点是,用户级线程包可以在不支持线程的操作系统上实现。过去所有的操作系统都属于这个范围,即使现在也有一些操作系统还是不支持线程。通过这一方法,可以用函数库实现线程。

线程在一个运行时系统的上层运行,该运行时系统是一个管理线程的过程的集合

运行时系统,也叫做运行时环境,该运行时系统提供了程序在其中运行的环境。此环境可能会解决很多问题,包括应用程序内存的布局,程序如何访问变量,在过程之间传递参数的机制,与操作系统的接口等等。编译器根据特定的运行时系统进行假设以生成正确的代码。通常,运行时系统将负责设置和管理堆栈,并且会包含诸如垃圾收集,线程或语言内置的其他动态的功能。

在用户空间管理线程时,每个进程需要有其专用的线程表。用来跟踪该进程中的线程。这些表和内核中的进程表类似,不过它仅仅记录各个线程的属性,如每个线程的程序计数器,堆栈指针,寄存器和状态等。该线程表由运行时系统管理。当一个线程转换到就绪状态或阻塞状态时,在该线程表中存放重新启动该线程所需的信息,与内核在进程表中存放进程的信息完全一样。

在这里插入图片描述

在用户空间实现线程的优势

在用户空间中实现线程要比在内核空间中实现线程具有这些方面的优势:考虑如果在线程完成时或者是在调用pthread_yield时,必要时会进行线程切换,然后线程的信息会被保存在运行时环境所提供的线程表中,然后,线程调度程序来选择另外一个需要运行的线程。保存线程的状态和调度程序都是本地过程,所以启动它们比进行内核调用效率更高。另一方面,不需要陷入内核,不需要进行上下文切换,也不需要对内存高速缓存进行刷新,这就使得线程调度非常快捷

用户级线程还有另一个优点。它允许每个进程有自己定制的调度算法。例如,在某些应用程序中,那些有垃圾收集线程的应用程序就不用担心线程会在不合适的时刻停止,这是一个优势。用户线程还具有较好的可扩展性,因为内核空间中的内核线程需要一些表空间和堆栈空间,如果内核线程数量比较大,容易造成问题。

在用户空间实现线程的劣势

尽管用户及线程包有更好的性能,但它也存在一些明显的问题。其中第一个问题是如何实现阻塞系统调用。假设在还没有任何击键之前,一个线程读取键盘。让该线程实际进行该系统调用是不可接受的,因为这会停止所有的线程。使用线程的一个主要目标是,首先要允许每个线程使用阻塞调用,但是还要避免被阻塞的线程影响其他线程

与阻塞调用类似的问题是缺页中断的问题,实际上,计算机并不会把所有的程序都一次性的放入内存中,如果某个程序发生函数调用或者跳转指令到了一条不再内存的指令上,就会发生页面故障,而操作系统将到磁盘上取回这个丢失的指令,这就称为页面故障。在对所需的指令进行定位和读入时,相关的进程就被阻塞,如果有一个线程引起页面故障,内核由于甚至不知道有线程存在,通常会把整个进程阻塞直到磁盘I/O完成为止

另外一个问题是,如果一个线程开始运行,该线程所在进程中的其他线程都不能运行,除非第一个线程自愿的放弃CPU,在一个单独的进程内部,没有时钟中断,所也不可能用轮转调度的方式调度线程。

对线程永久运行的问题的一个可能的解决方案是让运行时系统请求每秒一次的时钟信号(中断),但是这样对程序也是生硬和无须的。不可能总是高频率地发生周期性的时钟中断,即使可能,总的开销也是可观的。

在内核中实现线程

在图2-16b所示,此时不再需要运行时系统了。另外,每个进程中也没有线程表。相反,在内核中有用来记录系统中所有线程的线程表。当某个线程希望创建一个新线程或撤销一个已有线程时,它进行一个系统调用,这个系统调用通过对线程表的更新完成线程创建

内核的线程表保存了每个线程的寄存器,状态和其他信息。这些信息和在用户空间中(在运行时系统中)的线程是一样的,但是现在保存在内核中。这些信息是传统内核所维护的每个单线程进程信息(即进程状态)的子集。另外,内核还维护了传统的进程表,以便跟踪的状态。

所有能够阻塞线程的调用都以系统调用的形式实现,这与运行时系统过程相比,代价是相当可观的。当一个线程阻塞时,内核根据其选择,可以运行同一个进程中的另一个线程(若有一个就绪线程)或者运行另一个进程中的线程。而在用户级线程中,运行时系统始终运行自己进程中的线程,直到内核剥夺它的CPU(或者没有可运行的线程存在)为止

由于在内核中创建或撤销线程的代价比较大,某些系统采取“环保”的处理方式,回收其线程。当某个线程被撤销时,就把它标记为不可运行的,但是其内核数据结构没有受到影响。稍后,在必须创建一个新线程时,就重新启动某个旧线程,从而节省了一些开销。

内核线程不需要任何新的,非阻塞系统调用。另外,如果某个进程中的线程引起了页面故障,内核可以很方便地检查该进程是否有任何其他可运行的线程,如果有,在等待所需要的页面从磁盘读入时,就选择一个可运行的线程运行。这样做的缺点是系统调用的代价比较大,所以如果线程的操作比较多,就会带来很大的开销。

混合实现

人们已经研究了各种试图将用户级线程的优点和内核级线程的优点结合起来的方法。一种方法是使用内核级线程,然后将用户级线程与某些或者全部内核线程多路复用起来。

在这里插入图片描述
使用这种方法,内核只识别内核级别线程,并对其进行调度。其中一些内核级线程会被多个用户级线程多路复用。如同在没有多线程能力操作系统中某个进程中的用户级线程一样,可以创建,撤销和调度这些用户级线程。在这种模型中,每个内核级线程有一个可以轮流使用的用户级线程集合。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值