🌷🍁 博主 libin9iOak带您 Go to New World.✨🍁
🦄 个人主页——libin9iOak的博客🎐
🐳 《面试题大全》 文章图文并茂🦕生动形象🦖简单易学!欢迎大家来踩踩~🌺
🌊 《IDEA开发秘籍》学会IDEA常用操作,工作效率翻倍~💐
🪁🍁 希望本文能够给您带来一定的帮助🌸文章粗浅,敬请批评指正!🍁🐥
第十章 线程与线程控制
学习目的
通过对线程与线程控制的相关知识点的编程学习和锻炼,培养学生们对线程相关实例问题的分析与解决能力。
学习要求
了解:同一进程中的线程共享的资源。线程编程时存在的问题,进程与线程的比较,线程ID和线程是否相同的判断。
理解:线程退出时的清理机制;
掌握:线程的创建、终止和取消,detach以及线程属性。
学习方法
本章的线程概念较为抽象,需要学生较强的抽象思维能力。多线程编程部分需要学生上机实践。
概念和原理
10.1 线程概述
10.1.1 线程的引入
由于进程是一个资源的拥有者,因此在创建、撤销和切换中,系统必须为此付出较大的时间和空间的开销。
线程保留了并发的优点,避免了进程的高代价
10.1.2 线程的共享问题
▪ 进程内的所有线程共享进程的很多资源(这种共享又带来了同步问题)。
线程间共享 线程私有
进程指令 线程ID
全局变量 寄存器集合(包括PC和栈指针)
打开的文件 栈(用于存放局部变量)
信号处理程序 信号掩码
当前工作目录 优先级
用户ID
10.1.3 线程的数据共享
每个线程私有的数据和资源:线程ID、线程上下文(一组寄存器值的集合)、线程局部变量(存储在栈中)。
10.1.4 线程的互斥问题
对全局变量进行访问的基本步骤
a) 将内存单元中的数据读入寄存器
b) 对寄存器中的值进行运算
c) 将寄存器中的值写回内存单元
10.2 线程和进程的比较
10.2.1 线程和进程的比较
(1) 调度
在传统的操作系统中,进程作为拥有资源和独立调度、分派的基本单位。而在引入线程的操作系统中,则把线程作为调度和分派的基本单位,而进程作为资源拥有的基本单位。
(2) 并发性
在引入线程的操作系统中,不仅进程之间可以并发执行,而且在一个进程中的多个线程之间亦可并发执行,使得操作系统具有更好的并发性,从而能更加有效地提高系统资源的利用率和系统的吞吐量。
(3) 拥有资源
一般而言,线程自己不拥有系统资源(也有一点必不可少的资源),但它可以访问其隶属进程的资源,即一个进程的代码段、数据段及所拥有的系统资源,如已打开的文件、I/O 设备等,可以供该进程中的所有线程所共享。
(4) 独立性
同一进程中的不同线程共享进程的内存空间和资源。
同一进程中的不同线程的独立性低于不同进程。
(5) 系统开销
线程的切换只需要保存和设置少量的寄存器内容,不涉及存储器管理方面的操作。
(6) 支持多处理机系统
一个进程分为多个线程分配到多个处理机上并行执行,可加速进程的完成。
10.2.2 线程的属性
(1) 轻型实体
线程自己基本不拥有系统资源,只拥有少量必不可少的资源:TCB,程序计数器、一组寄存器、栈。
(2) 独立调度和分派的基本单位
在多线程OS中,线程是独立运行的基本单位,因而也是独立调度和分派的基本单位。
(3) 可并发执行
同一进程中的多个线程之间可以并发执行,一个线程可以创建和撤消另一个线程。
(4) 共享进程资源
它可与同属一个进程的其它线程共享进程所拥有的全部资源。
10.3 线程的状态与组成
10.3.1 线程的状态
线程运行时有以下3种状态:
- 执行状态:表示线程正获得CPU而运行;
- 就绪状态:表示线程已具备了各种运行条件,一旦获得CPU便可执行;
- 阻塞状态:表示线程在运行中因某事件而受阻,处于暂停执行的状态;
图10-1 线程的状态转换
10.3.2 线程的组成
线程必须在某个进程内执行
一个进程可以包含一个线程或多个线程
图10-2 单线程和多线程的进程模型
每个线程有一个TCB结构,即线程控制块,用于保存自己私有的信息,主要由以下部分组成:
▪ 一个唯一的线程标识符
▪ 一组寄存器 :包括程序计数器、状态寄存器、通用寄存器的内容;
▪ 线程运行状态:用于描述线程正处于何种运行状态;
▪ 优先级:描述线程执行的优先程度;
▪ 线程专有存储器:用于保存线程自己的局部变量拷贝;
▪ 信号屏蔽:对某些信号加以屏蔽。
▪ 两个栈指针:核心栈、用户栈
(1) 线程ID
同进程一样,每个线程也有一个线程ID
进程ID在整个系统中是唯一的,线程ID只在它所属的进程环境中也是唯一的。
线程ID的类型是pthread_t,在Linux中的定义如下:
typedef unsigned long int pthread_t
(/usr/include/bits/pthreadtypes.h)
(2) 获取线程ID
pthread_self函数可以让调用线程获取自己的线程ID
函数原型
▪ 头文件:pthread.h
▪ pthread_t pthread_self();
返回调用线程的线程ID
(3) 比较线程ID
Linux中使用整型表示线程ID,而其他系统则不一定
FreeBSD 5.2.1、Mac OS X 10.3用一个指向pthread结构的指针来表示pthread_t类型。
为了保证应用程序的可移植性,在比较两个线程ID是否相同时,建议使用pthread_equal函数
(4) pthread_equal函数
该函数用于比较两个线程ID是否相同
函数原型
▪ 头文件:pthread.h
▪ int pthread_equal(pthread_t tid1, pthread_t tid2);
若相等则返回非0值,否则返回0
(5) 进程/线程控制操作对比
应用功能 | 线程 | 进程 |
创建 | pthread_create | fork,vfork |
退出 | pthread_exit | exit |
等待 | pthread_join | wait、waitpid |
取消/终止 | pthread_cancel | abort |
读取ID | pthread_self() | getpid() |
同步互斥/通信机制 | 互斥锁、条件变量、读写锁 | 无名管道、有名管道、信号、消息队列、信号量、共享内存 |
10.4 线程的创建与终止
10.4.1 线程的创建
▪ 在多线程OS环境下,应用程序在启动时,通常仅有一个“初始化线程”线程在执行。
▪ 在创建新线程时,需要利用一个线程创建函数(或系统调用),并提供相应的参数。
- 如指向线程主程序的入口指针、堆栈的大小,以及用于调度的优先级等。
▪ 在线程创建函数执行完后,将返回一个线程标识符供以后使用
▪ Linux下线程创建
- Linux系统下的多线程遵循POSIX线程接口,称为pthread。
#include <pthread.h>
int pthread_create(
pthread_t *restrict tidp, //指向线程标识符的指针
const pthread_attr_t *restrict attr, //设置线程属性
void *(*start_rtn)(void), //线程运行函数的起始地址
void *restrict arg; ) //运行函数的参数
10.4.2 线程的终止
▪ 线程完成了自己的工作后自愿退出;
▪ 或线程在运行中出现错误或由于某种原因而被其它线程强行终止。
▪ 终止线程的方式有两种:
- 自愿退出 return , pthread_exit
void pthread_exit(void *rval_ptr);
由于pthread库不是Linux系统默认的库,连接时需要使用库libpthread.a,所以如果使用pthread_create、pthread_exit等函数时,在编译中要加-lpthread参数:
#gcc -o XXX -lpthread XXX.c
- 强行终止 pthread_cancel
▪ 父线程等待子线程终止
- 函数原型
- 头文件:pthread.h
- int pthread_join(pthread_t thread,void **rval_ptr);
- thread:需要等待的子线程ID
- rval_ptr:(若不关心线程返回值,可直接将该参数设置为空指针NULL)
- 若线程从启动线程通过return返回,rval_ptr指向的内存单元中存放的是线程的返回值
- 若线程被其它线程调用pthread_cancel取消,rval_ptr指向的内存单元存放常数PTHREAD_CANCELED
- 若线程通过自己调用pthread_exit函数终止,rval_ptr就是调用pthread_exit时传入的参数
- 调用该函数的父线程将一直被阻塞,直到指定的子线程终止
- 返回值
- 成功返回0,否则返回错误编号
▪ 取消线程
- 线程调用该函数可以取消同一进程中的其他线程(即让该线程终止)
- 函数原型
- 头文件: pthread.h
- int pthread_cancel(pthread_t tid);
- 参数与返回值
- tid:需要取消的线程ID
- 成功返回0, 出错返回错误编号
▪ 线程清理处理函数
- 线程清理处理函数的注册
- 头文件:pthread.h
- void pthread_cleanup_push(void (*rtn)(void *), void *arg);
- void pthread_cleanup_pop(int execute);
- 参数
- rtn:清理函数,无返回值,包含一个类型为指针的参数
- arg:当清理函数被调用时,arg将被传递给清理函数
10.5 线程的属性
(1) 线程属性
图10-3 POSIX规定的一些线程属性
(2) 初始化和销毁
▪ 函数原型
#include<pthread.h>
int pthread_attr_init(pthread_attr_t *attr);
int pthread_attr_destroy(pthread_attr_t *attr);
▪ 参数与返回值
- 成功返回0,否则返回错误编号
- attr:线程属性,确保attr指向的存储区域有效
- 为了移植性,pthread_attr_t结构对应用程序是不可见的,应使用设置和查询等函数访问属性
(3) 初始化线程属性对象
属性 | 缺省值 | 描述 |
scope | PTHREAD_SCOPE_PROCESS | 新线程与进程中的其他线程发生竞争 |
detachstate | PTHREAD_CREATE_JOINABLE | 线程可以被其它线程等待 |
stackaddr | NULL | 新线程具有系统分配的栈地址 |
stacksize | 0 | 新线程具有系统定义的栈大小 |
priority | 0 | 新线程的优先级为0 |
inheritsched | PTHREAD_EXPLICIT_SCHED | 新线程不继承父线程调度优先级 |
schedpolicy | SCHED_OTHER | 新线程使用优先级调用策略 |
(4) 获取线程栈属性
▪ 函数原型
#include<pthread.h>
int pthread_attr_getstack(
const pthread_attr_t *attr,
void **stackaddr, size_t *stacksize);
▪ 参数与返回值
- attr:线程属性
- stackaddr:该函数返回的线程栈的最低地址
- stacksize:该函数返回的线程栈的大小
- 成功返回0,否则返回错误编号
(5) 设置线程栈属性
▪ 函数原型
#include<pthread.h>
int pthread_attr_setstack(
const pthread_attr_t *attr,
void *stackaddr, size_t *stacksize);
▪ 当用完线程栈时,可以再分配内存,并调用本函数设置新建栈的位置
▪ 参数与返回值
- attr:线程属性
- stackaddr:新栈的内存单元的最低地址,通常是栈的开始位置;对于某些处理器,栈是从高地址向低地址方向伸展的,stackaddr就是栈的结尾
- stacksize:新栈的大小
- 成功返回0,否则返回错误编号
(6) pthread_detach函数
▪ 函数原型
- 头文件:pthread.h
- int pthread_detach(pthread_t tid);
▪ 参数与返回值
- tid:进入分离状态的线程的ID
- 成功返回0,出错返回错误编号
10.6 死锁
10.6.1 死锁的定义
如果一组进程中的每一个进程都在等待仅由该组进程中的其它进程才能引发的事件,那么该组进程是死锁的。
10.6.2 产生死锁的原因和必要条件
▪ 原因
a) 竞争不可抢占性资源引起死锁
b) 竞争临时性(消耗性)资源引起进行死锁
c) 进程推进顺序不当引起死锁
▪ 必要条件
a) 互斥条件 :进程对分配到的资源进行排它性使用。
b) 请求和保持条件 :进程已经保持了至少一个资源,但又提出了新的资源要求,而该资源又被其他进程占有,请求进程阻塞,但对已经获得的资源不释放。
c) 不剥夺条件 :进程已获得的资源,使用完之前不能被剥夺,只能用完自己释放。
d) 环路等待条件 :发生死锁时,必然存在进程—资源的环形链。
10.6.3 处理死锁的基本方法
(1) 预防死锁:设置某些限制条件,破坏四个必要条件(除第一个互斥条件外的其他条件)中的一个或几个。
优点:容易实现。
缺点:系统资源利用率和吞吐量降低。
(2) 避免死锁:在资源的动态分配过程用某种方法防止系统进入不安全状态。
优点:较弱限制条件可获得较高系统资源利用率和吞吐量。
缺点:有一定实现难度。
(3) 检测死锁:预先不采取任何限制,也不检查系统是否已进入不安全区,通过设置检测机构,检测出死锁后解除。
(4) 解除死锁:常用撤消或挂起一些进程,回收一些资源。
10.7 线程间的同步和互斥
为使系统中的多线程能有条不紊的运行,系统必须提供用于实现线程间同步和互斥的机制。在多线程OS中,通常提供多种同步机制:
10.7.1 互斥锁
▪ 互斥锁可以有两种状态, 即开锁(unlock)和关锁(lock)状态
▪ 对互斥量进行加锁以后,任何其他试图再次对互斥量加锁的线程都会被阻塞直到当前线程释放该互斥锁。
▪ Linux中的线程互斥锁
- int pthread_mutex_lock(pthread_mutex_t *mutex);
//返回时,互斥锁已被锁定。该线程使互斥锁锁住。如果互斥锁已被另一个线程锁定和拥有,则该线程将阻塞,直到互斥锁变为可用为止。
- int pthread_mutex_unlock(pthread_mutex_t *mutex);
//释放互斥锁,与pthread_mutex_lock成对存在。
10.7.2 条件变量
▪ 单纯的互斥锁用于短期锁定,主要是用来保证对临界区的互斥进入。而条件变量则用于线程的长期等待, 直至所等待的资源成为可用的。
▪ 使用步骤:
- 线程首先对mutex执行关锁操作,若成功便进入临界区,然后查找用于描述资源状态的数据结构,以了解资源的情况。
- 只要发现所需资源R正处于忙碌状态,线程便转为等待状态,并对mutex执行开锁操作后,等待该资源被释放;
- 若资源处于空闲状态,表明线程可以使用该资源,于是将该资源设置为忙碌状态,再对mutex执行开锁操作。
10.7.3 信号量机制
(1) 私用信号量
当某线程需利用信号量来实现同一进程中各线程之间的同步时,可调用创建信号量的命令来创建一私用信号量,其数据结构存放在应用程序的地址空间中。
(2) 公用信号量
公用信号量是为实现不同进程间或不同进程中各线程之间的同步而设置的。
10.8 Linux下的多线程编程
10.8.1 Linux下的多线程编程
(1) 多线程编程实例
#include <stdio.h>
#include <pthread.h>
void thread(void)
{
int i;
for(i=0;i<3;i++)
printf(“This is a pthread.\n”);
}
int main(void)
{
pthread_t id;
int i,ret;
ret=pthread_create(&id,NULL,(void *) thread,NULL);
if(ret!=0){
printf (“Create pthread error!\n”);
exit (1);
}
for(i=0;i<3;i++)
printf(“This is the main process.\n”);
pthread_join(id,NULL);
return (0);
}
执行:gcc example.c -lpthread -o example
-l参数用于指定编译时要用到的库
(2) 线程标识符 pthread_t
用来标识一个线程。
(3) 线程创建函数pthread_create
函数原型:
int pthread_create (pthread_t * thread_id, __const pthread_attr_t * __attr, void (__start_routine) (void *),void *__restrict __arg)
- 第一个参数为指向线程标识符的指针
- 第二个参数用来设置线程属性
- 第三个参数是线程运行函数的起始地址
- 最后一个参数是运行函数的参数
- 函数thread不需要参数,所以最后一个参数设为空指针。第二个参数也设为空指针,这样将生成默认属性的线程。
- 当创建线程成功时,函数返回0,若不为0则说明创建线程失败,常见的错误返回代码为EAGAIN和EINVAL。前者表示系统限制创建新的线程,例如线程数目过多了;后者表示第二个参数代表的线程属性值非法。
(4) pthread_join函数
函数原型:
int pthread_join (pthread_t __th, void **__thread_return)
- 第一个参数为被等待的线程标识符 。
- 第二个参数为一个用户定义的指针,用来存储被等待线程返回值。
- 这个函数是一个线程阻塞的函数,调用它的函数将一直等待到被等待的线程结束为止,当函数返回时,被等待线程的资源被收回。
(5) pthread_exit函数
函数原型:
void pthread_exit (void *__retval)
- 唯一的参数是函数的返回代码 。如果pthread_join中的第二个参数thread_return不是NULL,这个值将被传递给 thread_return。
- 需要注意的是:一个线程不能被多个线程等待,否则第一个接收到信号的线程成功返回,其余调用pthread_join的线程则返回错误代码ESRCH。
(6) 互斥锁
互斥锁用来保证一段时间内只有一个线程在执行一段代码。
重点
(1)线程清理机制;2)线程的属性。
这部分内容采用示例程序展示的方式教学,通过针对性的编写示例程序展示这些函数的使用,以及相应功能的实现。同时通过实验强化这部分知识的掌握。
难点
Linux多线程编程。
习题
1.比较线程和进程的区别。
答:(1) 调度
在传统的操作系统中,进程作为拥有资源和独立调度、分派的基本单位。而在引入线程的操作系统中,则把线程作为调度和分派的基本单位,而进程作为资源拥有的基本单位。
(2) 并发性
在引入线程的操作系统中,不仅进程之间可以并发执行,而且在一个进程中的多个线程之间亦可并发执行,使得操作系统具有更好的并发性,从而能更加有效地提高系统资源的利用率和系统的吞吐量。
(3) 拥有资源
一般而言,线程自己不拥有系统资源(也有一点必不可少的资源),但它可以访问其隶属进程的资源,即一个进程的代码段、数据段及所拥有的系统资源,如已打开的文件、I/O 设备等,可以供该进程中的所有线程所共享。
(4) 独立性
同一进程中的不同线程共享进程的内存空间和资源。
同一进程中的不同线程的独立性低于不同进程。
(5) 系统开销
线程的切换只需要保存和设置少量的寄存器内容,不涉及存储器管理方面的操作。
(6) 支持多处理机系统
一个进程分为多个线程分配到多个处理机上并行执行,可加速进程的完成。
2.死锁产生的主要原因有哪些?
答:a) 竞争不可抢占性资源引起死锁
b) 竞争临时性(消耗性)资源引起进行死锁
c) 进程推进顺序不当引起死锁
3.死锁的必要条件有哪些?
答:a) 互斥条件
b) 请求和保持条件
c) 不剥夺条件
d) 环路等待条件
- 如何解决死锁?
答:(1) 预防死锁:设置某些限制条件,破坏四个必要条件(除第一个互斥条件外的其他条件)中的一个或几个。
(2) 避免死锁:在资源的动态分配过程用某种方法防止系统进入不安全状态。
(3) 检测死锁:预先不采取任何限制,也不检查系统是否已进入不安全区,通过设置检测机构,检测出死锁后解除。
(4) 解除死锁:常用撤消或挂起一些进程,回收一些资源。
原创声明
=======
作者: [ libin9iOak ]
本文为原创文章,版权归作者所有。未经许可,禁止转载、复制或引用。
作者保证信息真实可靠,但不对准确性和完整性承担责任。
未经许可,禁止商业用途。
如有疑问或建议,请联系作者。
感谢您的支持与尊重。
点击
下方名片
,加入IT技术核心学习团队。一起探索科技的未来,共同成长。