编程知识 cdmana.com

操作系统概念学习笔记 9 线程

操作系统概念学习笔记 9

线程


概述

单个进程可以包括多个控制线程。

线程 ——一种CPU利用的基本单元,它是形成多线程计算机的基础。

线程是CPU使用的基本单元,它由线程ID、程序计数器、寄存器集合和栈组成。它与属于统一进程的其他线程共享代码段、数据段和其他操作系统资源。

一个传统重量级的进程只有单个控制线程,如果进程有多个控制线程,那么能同时做多个任务。

单线程与多线程

动机

一个应用程序通常是作为一个具有多个控制线程的独立进程实现的。如一个忙碌的网页服务器如果有多个(或数千个)客户并发访问它,可以建立多个线程而非进程,因为创建进程很耗时间,而单个线程的进程一次只能处理一个请求。

线程在远程过程调用(RPC)系统中个也有很重要的作用,通常,PRC服务器是多线程的。当一个服务器接收到消息,它使用独立线程处理消息,这允许服务器能处理多个并发请求。

现代许多操作系统都是多线程的,少数线程在内核中运行,每个线程完成一个指定的任务。

多线程的优点

  1. 响应度高:即使其部分阻塞或执行较冗长操作,该程序仍能继续执行,从而增加了对用户的相应程度。
  2. 资源共享:线程默认共享它们所属进程的内存和资源。代码和数据共享的优点是它允许一个应用程序在同一地址空间有多个不同的活动线程。
  3. 经济:进程创建所需要的内存和资源的分配比较昂贵。由于线程能共享它们所属进程的资源,所以创建和切换线程会更为经济。
  4. 多处理器体系结构的利用:多线程的优点之一是能充分使用多处理器体系结构。以便每个进程能并行运行在不同的处理器上。不管有多少CPU,单线程进程只能运行在一个CPU上,在多CPU上使用多线程加强了并发功能。

多线程模型:

有两种不同的方法来提供线程支持:用户层的用户线程和内核层的内核线程。用户线程受内核支持,而无需内核管理;而内核线程由操作系统支持和管理。事实上所有当代操作系统都支持内核线程。在用户线程和内核线程之间必然存在一种关系。

多对一模型:

多对一模型将许多用户级线程映射到一个内核线程。线程管理由线程库在用户空间进行的,因而效率比较高。但是如果一个线程执行了阻塞系统调用,那么整个线程会阻塞。因为任意时刻只能有一个线程能够访问内核,多个线程不能并行运行在多处理器上。

一对一模型:

一对一模型每个用户线程映射到一个内核线程。该模型在一个线程执行阻塞系统调用时,能允许另一个线程继续执行。它也允许多个线程能并行运行在多处理器系统上,这种模型的唯一缺点是每创建一个用户线程就会创建一个相应的内核线程。由于创建内核线程的开销会影响应用程序的性能,所以这种模型的绝大多数实现限制了操作系统所支持的线程数量。

多对多模型:

多对多模型多路复用了许多用户线程到同样数量或更小数量的内核线程上。多对多模型没有这两者的缺点:开发人员可创建任意多的用户线程,并且相应内核线程能在多处理器系统上并发执行。而且当一个线程执行阻塞系统调用时,内核能调度另一个线程来执行。

一个流行多对多模型的变种仍然多路服用了许多用户线程到同样数量或更小数量的内核线程上,但也允许将一个用户线程绑定到某个内核线程上。这个变种有是被称为二级模型。

thread library):为程序员提供创建和管理线程的API。主要有两种方法来实现线程库。

(1)在用户空间中提供一个没有内核支持的库,此库的所有代码和数据结构都存在于用户空间中。调用库中的一个函数知识导致用户空间中一个本地函数调用,而不是系统调用。

(2)执行一个有操作系统直接支持的内核级的库。此时,库的代码和数据结构存在于内核空间中。调用库中的一个API函数通常会导致对内核的系统调用。

目前使用的三种主要的线程库是:

(1)POSIX Pthread (2)Win32 (3)Java

Pthread作为POSIX标准扩展,可以提供用户级或内核级的库。Win32线程库是适用于Windows操作系统的内核级线程库。Java线程API允许线程在java程序中直接创建和管理。然而,由于大多数JVM实例运行在宿主操作系统之上,Java线程API通常采用宿主系统上的线程库来实现。

Pthread

Pthread是由POSIX标准为线程创建和同步定义的API。这是线程行为的规范,而不是实现。操作系统设计者可以根据意愿采取任何形式来实现。

所有Pthread程序都需要包括pthread.h头文件。

Pthread_t tid声明了所常见线程的标识符。每个线程都有一组属性,包括栈大小和调度信息。

Pthread_attr_t attr表示线程的属性,通过函数调用pthread_attr_init(&attr)来设置这些属性。

Pthread_create()创建一个独立线程。除了传递线程标识符和线程属性外,还要传递函数名称。

pthread_join()函数等待

pthread_exit()完成

Pthread实例程序:

以下示例实验程序实现并发的两个线程合作将整数 X 的值从 1 加到 10 的功 能。它们通过管道相互将计算结果发给对方。

/*
* description
:  tpipe.c
* copyright
: (C)2015  by 孙铭超
* Function
: 利用管道实现在在线程间传递整数
*/
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <pthread.h>
void task1(int *); //线程 1 执行函数原型
void task2(int *); //线程 2 执行函数原型
int pipe1[2],pipe2[2];//存放两个无名管道标号
pthread_t thrd1,thrd2;//存放两个线程标识
int main(int argc,char *arg[])
{
int ret;
int num1,num2;
//使用pipe()系统调用建立两个无名管道。建立不成功程序退出,执行终止
if(pipe(pipe1) < 0){
perror("pipe not create");
exit(EXIT_FAILURE);
}
if(pipe(pipe2) < 0){
perror("pipe not create");
exit(EXIT_FAILURE);
}
//使用pthread_create系统调用建立两个线程。建立不成功程序退出,执行终止
num1 = 1 ;
ret = pthread_create(&thrd1,NULL,(void *) task1,(void *) &num1);
if(ret){
perror("pthread_create: task1");
exit(EXIT_FAILURE);
}
num2 = 2 ;
ret = pthread_create(&thrd2,NULL,(void *) task2,(void *) &num2);
if(ret){
perror("pthread_create: task2");
exit(EXIT_FAILURE);
}
//挂起当前线程切换到thrd2 线程
pthread_join(thrd2,NULL);
//挂起当前线程切换到thrd1 线程
pthread_join(thrd1,NULL);
exit(EXIT_SUCCESS);
}
//线程 1 执行函数,它首先向管道写,然后从管道读
void task1(int *num)
{
int x=1;
//每次循环向管道 1 的 1 端写入变量X的值,并从
//管道 2 的 0 端读一整数写入X再对X加 1,直到X大于 10
do{
printf("thread%d read: %d\n",*num,x++);
write(pipe1[1],&x,sizeof(int));
read(pipe2[0],&x,sizeof(int));
}while(x<=9);
//读写完成后,关闭管道
close(pipe1[1]);
close(pipe2[0]);
}
//线程 1 执行函数,它首先从管道读,然后向管道写
void task2(int * num)
{
int x;
//每次循环从管道 1 的 0 端读一个整数放入变量X中,
//并对X加 1 后写入管道 2 的 1 端,直到X大于 10
do{
read(pipe1[0],&x,sizeof(int));
printf("thread2 read: %d\n",x++);
write(pipe2[1],&x,sizeof(int));
}while( x<=9 );
//读写完成后,关闭管道
close(pipe1[0]);
close(pipe2[1]);
}

运行结果:

sora@sora:~/SORA/program/Pthread$ gcc -g -c demo.c
sora@sora:~/SORA/program/Pthread$ gcc demo.o -l pthread -o demo
sora@sora:~/SORA/program/Pthread$ ./demo 
thread1 read: 1
thread2 read: 2
thread1 read: 3
thread2 read: 4
thread1 read: 5
thread2 read: 6
thread1 read: 7
thread2 read: 8
thread1 read: 9
thread2 read: 10

Win32线程

Win32线程库创建线程的技术在某些方面类似与Pthread技术。

Win32线程:Win32 API必须包括windows.h头文件

线程的创建使用了CreateThread() 将一组线程的属性传递给此函数。

在Pthread程序中采用的pthread_join()语句实现线程等待,在Win32中采用同等功能的函数WaitForSingleObject(),从而使创建者线程阻塞。

Java线程

Java线程:线程是Java程序中程序执行的基本模型。

所有Java程序至少有一个控制线程组成,即使是一个只有main()函数的简单Java程序也是在JVM中作为一个线程运行的。

Java程序中有两种创建线程的技术。

(1)创建一个新的类,它从Thread类派生,并重载它的run()函数。

(2)定义一个实现Runnable()接口的类。当一个类执行runnable()时,他必须定义run()函数,而实现run()函数的代码被作为一个独立的线程执行。

创建Thread对性并不会创建一个新的线程,实际上是用start()函数来创建新线程。为新的对象调用start()函数需要做两件事:

一是在JVM分配内存并初始化新的线程;

二是调用run()函数,实现线程适合在JVM中运行 (注意,从不直接调run()函数,而是调用start()函数,然后它再调用run()函数)。

与pthread_join()相对应的java中有join()函数。

三者比较:

在Win32和Pthread共享数据很方便,可以将共享数据简单的声明为全局数据。而对于Java没有全局数据的概念,在Java程序中如果两个或更多的线程需要共享数据,通过向相应的线程传递对共享对象的引用来实现。

多线程问题

系统调用fork()和exec()

在多线程程序中,系统调用fork()和exec()的语义有所改变。

如果程序中一个进程调用fork(),那么新进程会复制所有线程,还是新进程只有单个线程?有的UNIX系统有两种形式的fork(),一种复制所有线程,另一种只复制调用了系统调用fork()的线程。

Exec()工作方式:如果一个线程调用系统调用exec(),那么exec()参数所指定的程序会替换整个进程,包括所有线程。

如果调用fork()之后立即调用exec(),那么没有必要复制所有线程,因为exec()参数所指定的程序会替换整个进程。在这种情况下,只复制调用线程比较适当。不过,如果在fork()之后另一进程并不调用exec(),那么另一进程就应复制所有进程。

取消

线程取消(thread cancellation)是在线程完成之前来终止线程的任务。

要取消的线程通常称为目标线程。目标线程的取消可在如下两种情况下发生:

一是异步取消(asynchronous cancellation):一个线程立即终止目标线程。

二是延迟取消(deferred cancellation):目标线程不断地检查它是否应终止,这允许目标线程有机会以有序方式来终止自己。

如果资源已经分配給要取消的线程,或者要取消的线程正在更新与其他线程所共享的数据,那么取消会有困难,对于异步取消尤为麻烦。操作系统回收取消线程的系统资源,但是通常不回收所有资源。因此,异步取消线程并不会使所需的系统资源空闲。相反采用延迟取消时,允许一个线程检查它是否是在安全的点被取消,pthread称这些点为取消点(cancellation point)

信号处理

信号处理:信号在Unix中用来通知进程某个特定时间已发生了,信号可以同步或异步接收。所有有信号具有同样的模式:

(1)信号有特定事件的发生所产生

(2)产生的信号要发送到进程

(3)一旦发送,信号必须交易处理。

同步信号的例子包括访问非法内存或被0除。在这种情况下,如果运行程序执行这些动作,那么就产生信号,同步信号发送到执行操作而产生信号的同一进程(同步的原因)。

当一个信号由运行进程之外的事件产生,那么进程就异步接收这一信号。这种信号的例子包括使用特殊键(Ctrl + C)或者定时器到期。通常,异步信号被发送到另一个进程。

每个信号可能由两种可能的处理程序中的一种来处理:

(1)默认信号处理程序

(2)用户定义的信号处理程序

每个信号都有一个默认信号处理程序,当处理信号是在内核中运行的,这种默认动作可以用用户定义的信号处理程序来改写。信号可以按照不同的方式处理。有的信号可以简单的忽略(如改变窗口大小),有的需要终止程序来处理(非法内存访问)

单线程程序的信号处理比较直接,信号总是发送给进程。

当多线程时,信号会

(1)发送信号到信号所应用的线程

(2)发送信号到进程内的每个线程

(3)发送信号到进程内的某些固定线程

(4)规定一个特定线程以接收进程的所有信号。

发送信号的方法依赖于信号的类型。

大多数线程版Unix允许线程描述它会接收什么信号和拒绝什么信号。,因为信号只能处理一次,所以信号通常发送到不拒绝它的第一个线程。

pthread还提供了pthread_kill函数,此函数允许限号被传送到一个指定线程。

Windows,通过异步过程调用(asynchronous procedure call,APC)来模拟信号。

线程池

多线程服务器有一些潜在问题:第一个是关于处理请求之前用以创建线程的时间,以及线程在完成工作之后就要被丢弃这一事实。第二个,如果允许所有并发请求都通过新线程来处理,那么将没法限制在系统中并发执行的线程的数量。无限制的线程会耗尽系统资源。解决这一问题是使用线程池。

线程池的思想是在进程开始时创建一定数量的线程,并放入到池中以等待工作。当服务器收到请求时,他会唤醒池中的一个线程,并将要处理的请求传递给他,一旦线程完成了服务,它会返回到池中在等待工作。如果池中没有可用的线程,那么服务器会一直等待直到有空线程为止。

线程池的优点:

(1)通常用现有线程处理请求要比等待创建新的线程要快

(2)线程池限制了在任何时候可用线程的数量。

线程池中的线程数量由系统CPU的数量、物理内存的大小和并发客户请求的期望值等因素决定。比较高级的线程池能动态的调整线程的数量,以适应具体情况。

线程特定数据

同属一个进程的线程共享进程数据。

在某些情况下每个线程可能需要一定数据的自己的副本,这种数据称为线程特定数据。可以让每个线程与其唯一的标识符相关联。

调度程序激活

内核与线程库的通信问题,就是要讨论多对多模型。这种协调允许动态调整内核线程的数量以保证其最好的性能。

在用户内核线程之间设置一种中间数据结构。轻量级进程(LWP),他表现为一种应用程序可以调度用户线程来运行的虚拟处理器。每个LWP与内核线程相连,该内核线程被操作系统调度到物理处理器上运行,如果内核线程阻塞,LWP阻塞,与LWP相连的用户线程也阻塞。如果在单处理器上,一次只能运行一个线程,则只需要一个LWP就够了,但I/O密集的应用程序可能需要多个LWP来执行。

一种结局用户线程库与内核间通信的方法为调度器激活(scheduler activation),内核提供一组虚拟处理器(LWP)给应用程序,应用程序可调度用户进程到一个可用的虚拟处理器上。

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

版权声明
本文为[种花家的奋斗兔]所创,转载请带上原文链接,感谢
https://cloud.tencent.com/developer/article/1747159

Scroll to Top