linux:《线程概念》《线程控制》《线程安全》《线程应用》

1.线程概念:
线程:线程是进程中的一条执行流
进程是一个运行中的程序,要完成一个/多个任务,在以前进程中,多个任务只能串行完成,因为以前的进程中是只有一条执行流的进程
若一个进程中有多条执行流,则多个任务可以同时并行处理(CPU资源足够),提高任务处理效率

一个生产工厂,进程就像工厂/线程就像工厂中干活的工人
linux如何实现一个进程中有多个执行流,执行流又是什么?
在linux中,pcb是一 个程序动态运行的描述, 一个pcb就可以调度一段代码的执行, 因此在linux中执行流就是pcb
反过来说,linux中的执行流是通过pcb实现的,一个进程中可以有多个pcb,并且这些pcb共享进程中的大部分资源

linux中:
在学习进程的时候,认为进程就是pcb,是程序运行动态的描述,通过pcb实现程序运行的调度管理。
在学习线程的时候,认为pcb是一个轻量级进程 (pcb是进程,相较于传统pcb更加轻量化 共享了一个程中的大部分资源)。 因为在Iinux中使用pcb实现了进程中的执行流。一个进程中可以有多个执行流。

创建线程的时候会伴随在内核中创建一个pcb作为一条执行流实现一段程序的运行调度管理。
因此有人说,linux下的线程就是一个轻量级进程, 也有人说linux下没有真正的线程,线程是一个轻量级进程。

线程是cpu调度的基本单元/进程是资源分配的基本单位
线程,是进程中的一条执行流,是cpu调度执行的基本单元, 因为linux 下执行流通过pcb实现,并且一个进程中可以有多个pcb,共享进程资源,因此linux下的线程也叫做轻量级进程,是一 个pcb (这些pcb中有 一个共同的线程组id,标识它们属于同一个进程) .

进程,是程序运行时操作系统进行资源分配的基本单元
linux下从不同的角度阐述了进程资源分配和线程调度执行
一个工厂中有大量的设备,工厂中有很多工人,工人操作设备完成产品的生产

线程之间的独有与共享:
独立:栈, 寄存器(上下文信息),优先级,信号屏蔽字,标识符
共享:虚拟地址空间(代码段/数据段),文件描述符表,信号处理方式,errno,工作路径,用户id/组id
在这里插入图片描述
多线程/多进程进行多任务处理的优缺点分析:
多任务处理中,如果是单执行流,则只能串行处理若是多执行流处理,则可以并行处理(cpu资源足够) ,提高处理效率
在CPU密集型程序中,大量的CPu计算型任务需要处理,并行就可以提高效率 (10件事,两个人同时干总比一 个人来的快)
在IO密集型程序中,大量的IO操作, IO会有阻塞操作,一个无法完成就无法发起下一个IO请求, 多执行流发起请求就可以一个线程的阻塞不影响其他执行流
1.线程间通信更加灵活(包含进程间通信方式之外,还可以通过全局数据/函数传参的形式进行通信)
2.线程的创建与销毁成本要更低
3.同一个进程中的线程间调度成本更低

多进程:更加稳定,健壮(因为一些系统调用接口以及异常都是针对整个进程产生效果的:exit /异常)
在进行多任务处理的时候,并非执行流越多越好(执行流过多有时反而增加了调度成本)
在CPU密集型程序中:执行流的个数最好是cpu核心数+1 (+1是为了防止某个执行流阻塞, 而顶包作用的)
在IO密集型程序中:进程对cpu要求并不高,因此执行流就不受限与cpu核心数。

线程控制:线程创建/终止/等待/分离的接口操作
操作系统并没有提供创建线程的系统调用接口(这也是为什么有人说linux中是没有线程的原因) , 而是大佬们封装的一套库函数实现线程控制的各个操作。
在程序中程序员通过库函数创建线程,实际上是创建了一个用户态线程, 并且伴随在内核中创建了一个轻量级进程pcb实现线程的调度。
线程创建:创建一个执行流,能够独立完成某个任务
int pthread_ create(pthread t *tid, pthread attr t attr,void* ('thread. routine)(void *arg), void *arg);
tid: 输出型参数,用于获取创建的线程的id
attr:线程属性,通常置NULL
thread
routine: 线程的入口函数,线程的执行流运行的就是这个函数,函数运行完毕则线程退出
arg:最终thread_routine的唯一参数传递给栈的数据
返回值:成功返回0,失败返回错误编码——非0值
创建新的线程之后,主线程与普通线程谁先运行是不一定的, 取决于cpu调度
创建线程之后返回的tid实际上是一个地址

tid存储的数据就是,指定线程局部存储空间的首地址,通过这个tid就能找到这个线程的信息,进而对线程进行各种操作

pcb->pid: pcb是一个轻量级进程-LWP.这个pcb中的pid实际上也可以称作为轻量级进程id
pcb->tgid: 线程组id, 一个进程中的一个或多个pcb会组成线程组,线程组id等于进程中首线程的pcb—>pid
在这里插入图片描述
用户态线程与轻量级进程的关系:用户态线程就是轻量级进程在用户态的一些描述
在这里插入图片描述
线程终止:如何退出一个线程
线程入口函数运行完毕,线程就会退出
入口函数中return,退出线程
使用接口 pthread_exit(void *retval)—在任意位置调用都会退出当前的调用线程,retval—线程的退出返回值
被动退出:取消一个线程 pthread_cancel(pthread_t tid),使tid指定的线程退出,也可以在任意位置调用

注意事项:
主线程退出,并不会使进程退出,所有的线程都退出了,进程才会退出
main函数中return或者调用exit接口退 出的都是进程
进程退出会导致所有的线程都退出
线程等待: 等待一个指定的线程退出,获取这个退出线程的返回值,并且回收这个退出线程的资源
线程有个属性,默认是joinable状态,处于joinable状态的线程退出之后,不会自动释放资源,需要被其它线程等待
pthread_join(pthread_t tid, void **retval); —阻塞函数,若指定的线程没有退出则会一直等待
tid: 表示要等待的指定的线程
retval: 二级指针,用于获取线程返回的一级指针返回值
int testinta){ int test1(int *a) {
a= 10; *a= 10;
} }
intb= 100; test(b); printf(b) --传值修改,对外部变量没有影响
int b= 100; test(&b); printf(b) —获取数据需要传入一块空间的地址

char *ptr = *nihao’ /char ptr[] = "nihao"有什么区别? ??
“nihao” 这是一个常量,存储在代码段
char *ptr = “nihao"是将"nihao” 常量的地址赋值给ptr / char prI = "nihao’ ptr申请一块局部存储空间存储"nihao"字符串
retumptr; 第一种是返回常量的地址; ;_第二种是返回局部存储的地址 (局部存储一旦函数运行完毕就会被释放)
线程在返回局部存储的数据的时候要小心----尽量不要返回局部存储的数据
线程分离:将线程的属性从joinable状态设置为detach状态

处于detach状态的线程, 退出后,会自动释放资源,不需要被等待,也不能被等待(因为资源已经释放了,获取不到返回值了)

有一个线程,我不想获取返回值,也不想等待他,因为等待需要一个线程一 直阻塞等待才行, (不想获取返回值, 为什么非得等着呢? ?,但是不等又会造成资源泄漏)
pthread. detach(pthread. t tid); 分离指定的线程
分离一个线程,不定非要在创建线程后直接分离,也可以在任意位置分离(只要知道指定线程的tid即可,线程分离不过就是设置状态)
线程分离/线程等待,两种状态只能二选其一,我们要根据使用场景而定

线程安全:描述一个线程中的操作是否是安全的,主要讨论在多执行流处理情况下
在多执行流中,对临界资源进行争抢访问,而不会造成二义或逻辑异常

如何实现线程安全:通过同步与互斥实现线程安全
同步:通过条件判断实现对资源获取的合理性
互斥:通过同一时间只有一条执行流能够访问资源实现资源访问的安全性

互斥如何实现:互斥锁
互斥锁:就是一个只有0/1的计数器,描述临界资源访问的两种情况(可访问/不可访问)。一个执行流在访问期间就需要将资源状态标记为不可访问,访问完毕之后,将状态标记为可以访问
这些访问同一临界资源的执行流都要在访问资源之前,先去访问互斥锁,判断访问状态
在这里插入图片描述
操作流程及接口介绍
1.定义互斥锁变量:
pthread_mutex_t mutex=PTHREAD_MUTEX_INTIALIZER;
2.初始化互斥锁变量:
pthread_mutex_init(pthread_mutex_t *mutex, pthread_mutexattr_t *attr)
3.在访问临界资源之前加锁
pthread_ mutex locklptreaed. mutex t *mutex)如果不能加锁则阻塞等待
pthread_ mutex trylockpthread mutex. t *mutex);如果不能加锁则报错返回-- EBUSY
4.对临界资源访问完毕之后记得解锁
pthread_mutex_unlock(pthread_mutex_t *mutex)
5. 如果不使用互斥锁了则释放资源
pthread _mutx destroy(pthread _mutx t *mutex);

注意事项:
1.加锁保护的永远只有我们的临界资源的操作,尽量不要保护不相干操作–增加锁冲突后的等待时间,降低了程序效率
2.加锁之后,在任意有可能退出线程的地方都要进行解锁操作(否则其它线程有可能卡死在加锁上)
3.互斥锁只能保护资源访问的安全性,但是它不能保证访问的合理性
4.所有的线程访问同一临界资源,必须访问同一个互斥锁进行保护

死锁:多个执行流因为锁资源的争抢,但是因为推进顺序不当,陷入互相等待,导致程序流程无法继续推进
死锁产生的四个必要条件:

  1. 互斥条件同一时间只有一个执行流能够加锁
  2. 不可剥夺条件- - -哪个执行流加的锁, 必须哪个执行流进行解锁,其它执行流无法解锁
  3. 请求与保持条件-- 我加了A锁,然后去加B锁,如果不能加B锁,也不释放A锁
  4. 环路等待条件-- 我加了A锁,然后去加B锁,对方加了B锁,然后去加A锁(加锁顺序不当引起的)

如何预防:预防/破坏死锁产生的必要条件(如果加不了B锁,就将A锁释放掉 / 保证所有执行流加锁顺序一致)
如何避免:银行家算法/ 死锁检测算法

银行家算法:将系统分为安全/ 不安全状态,都有哪些锁/ 哪些执行流分配了哪些琐 / 哪个执行流想要加哪个锁
公分配给执行法想更的新之后身委会造成环路笔待是系统运行讲入不安全状太 如果有可能则不能分配

同步:通过条件判断,决定执行流是否能够获取资源,保证资源获取的合理性
同步的实现:条件变量—提供了一个pcb等待队列,提供了两个接口(一个让进程等待/ 一个唤醒线程)
条件变量在实现同步的时候,自身并没有提供条件判断的接口,也就是说,条件变量也不知道什么时候让线程休眠,什么时候唤醒线程 获取资源条件是否满足的判断需要程序员在程序中自己进行

接口介绍:
1.定义条件变量pthread_ cond. t cond = PTHREAD COND INITIALIZER;
2.初始化条件变量pthread. cond init(pthread. cond. t *cond, pthread cndattrt attr);
3.在执行流不满足资源获取条件的时候调用接口使执行流阻塞等待
pthread
cond wit(pthread, cond _t *cond, pthread. mutex t *mutex) —条件变量需要搭配互斥锁一起使用— 阻塞等待
pthread_cond timedwait(pthread_cond_t *cond,pthread_mutex_t *mutex,struct timespec *time)等待超时就会报错返回–ETIMEDOUT

4.在执行流满足森取资源多性之后的智壁待的执行流
pthread, cond sial(pthreaed cond t *cond); – 至少唤醒一个执行流(唤醒的有可能不只是一个)
pthread. cond. broadcast(pthread cond. t cond); --唤醒所有执行流
5.不使用条件变量则释放资源pthread _cond destroy(pthread cond. ↑
cond);
在这里插入图片描述
注意事项:
1.条件变量需要搭配互斥锁一起使用(临界资源是否能够进行获取的条件判断也涉及到对临界资源的访问,需要被保护)
2.临界资源获取条件的判断必须是一个while循环,而不是if判断(因为每次唤醒至少唤醒一个线程,其他线程被唤醒后就会卡在锁上,等待解锁之后,有可能出现相同的角色抢到锁,然后在没有资源的情况下去获取资源进行处理)
3.不同角色的线程需要使用多个条件变量,让步同线程在等待时挂在不同条件变量的pcb等待队列上(防止唤醒的时候如果都等待在同一个队列中出现唤醒角色错误的问题)

生产者与消费者模型:是一种设计模式,是一种针对典型场景设计的解决方案
应对场景:在程序中既要进行资源获取,又要进行资源处理(包含多种功能)的场景适用于生产者与消费者模型
生产者与消费者模型:一个场所,两种角色,三种关系
两种角色的执行流(生产者/消费者),各自完成各自模块任务,中间通过线程安全的任务队列实现数据交互
生产者获取到数据放到线程安全的任务队列中,消费者线程从队列中取到数据进行处理
优点:解耦合,支持忙闲不均,支持并发
实现:实现一个线程安全的队列+两种不同任务处理的角色线程

信号量:本质就是一个计数器+pcb队列
计数器就是用于对资源计数,实现线程/进程间的同步与互斥
信号量实现同步:通过自身计数器对资源计数,在获取资源之前通过判断是否获取合理,不合理再阻塞,合理则访问
信号量实现互斥:模拟资源数量只有一个,则表示只有一个执行流能访问资源,访问时资源数量-1,访问完毕后,资源数量+1,实现同一时间只有一个执行流能够访问资源

具体操作:
1.定义信号量:sem_t
2.初始化信号量(根据实际资源数量而定)sem_init(sem_t *sem,int pshared,int val)
pshared:决定信号量用于进程间还是线程间(两种实现方式不同);
val:就是初始资源的数量
3.在获取之前先访问信号量:
sem_wait(sem_t *sem);(计数<=0则阻塞;计数>0,则-1后返回)
int sem_trywait(sem_t *sem);若无法访问则立即报错返回
int sem_timedwait(sem_t *sem,const struct timespec *abs_timeout);等待超时后报错返回
4.资源数量增加后sem_post(sem_t *sem)计数+1;唤醒一个等待的线程
5.销毁信号量 sem_destroy(sem_t *sem)
纯使用信号量实现生产者与消费者模型----主要是实现一个线程安全的环形队列

class RingQueuel
std::vector_array;//实现环形队列的数组
int. caparir /决定环形队列中的节点数量
int _write_step;//当前写入位置的下标 _write_stepread_step----表示队列中没有数据
int _read_step;//当前读取位置下标(write_step+1)%_capacity
_read_step则表示队列中数据满了

sem_t sem_lock;//用于实现互斥,保护_array/_capacity/write_step/read_step的操作
semt. sem
dta;//有效数据空间的个数,对数据资源进行计数有数据空间,才能出队—对于消费者来说通过数据空间计数判断是否出队合理

sem.t. sem_ idle//空闲空间的计数—有空闲空间才能入队- -对于生产者来说通过空闲空间计数判断是否入队数据合理

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值