操作系统——多线程(上)

思维:考虑多线程的问题的时候,需要有全局思维:(从程序的角度,去考虑多个线程在同一时间都可能做的事情)

线程概念
线程控制
线程安全
同步和互斥
死锁
生产者与消费者模型
线程池

线程概念

pid_t pid: 轻量级进程id
pid_t tgid: 轻量级进程组id
linux内核当中是没有线程的概念的,而是轻量级进程的概念:LWP
通俗的线程概念其实是C库当中的概念 libc.so.6

主线程:轻量级进程 struct task_struct{...};
pid_t pid
pid_t tgid
pid == tgid
工作线程:轻量级进程 struct task_struct{...};
pid_t pid
pid_t tgid
pid 一定是不一样的
tgid 和主线程当中的 tgid是一样的

线程的独有和共享
共享:
文件描述符表
信号的处理方式
当前工作目录
用户id和组id
在这里插入图片描述
在这里插入图片描述
代码在运行的时候并行的跑,但是我们在编写代码的时候,一定要考虑时序的问题!

线程控制

线程创建

int pthread_create(pthread_t* thread, const pthread_attr_t* attr, void*
				   (*start_routine)(void *) void* arg);    

pthread_t : 线程标识符,是一个出参
pthread_attr_t: 线程属性 栈的大小 不设置线程的属性,一般传值为NULL, 采用默认属性
start_routine : 本质是函数指针,保存线程入口函数的地址
arg : 给线程入口函数传参
返回值:
创建成功:等于0
创建失败:小于0
pstack [pid] : 查看进程当中各个线程的执行调用堆栈
top
“1” : 可以查看各个CPU的负载
top -H -p [pd] : 查看各个线程的工作状态
传递参数
临时变量:
1.临时变量的生命周期
2.临时变量的值的改变
3.传递临时变量有可能导致越界的问题
· 注意:不推荐大家传递临时变量
结构体对象:参考临时变量
结构体指针:参考临时变量
线程终止
1.从入口函数的return返回,该线程就退出掉了
2.void pthread_exit(void* retval);
retval: 返回信息,可以给也可以不给,是返回给等待线程退出的执行流的,如果不
给,则传递NULL.谁调用,谁退出!
3.int prhread_cancel(pthread_t thread);
thread:线程标识符
调用该函数的执行流可以取消其他线程,但是需要知道其他线程的线程标识符。也
可以执行取消自己,传入自己的线程标识符
获取自己的线程标识符: pthread_self()
注意:线程在创建出来的时候,属性种默认是joinable属性(意味着线程退出的时候需要
其他执行流来回收线程资源)
线程等待
int pthread_join(pthread_t thread, void** retval);
thread:要等待的线程标识符
retval:
return : 接收入口函数的返回值
pthread_exit: 接收pthread_exit函数的参数
pthread_cancel : void** 当中保存一个常数PTHREAD_CANCELED
内核当中PTHREAD_CANCELED :#define PTHREAD_CANCELED (void*)(-1)
注意:调用该函数的执行流在等待线程退出的时候,该执行流是阻塞在pthread_join函数
当中的
线程分离
1.作用:改变线程属性,将joinable属性改变成为detach属性,当线程退出的时候,不需要其他线程在来回收退出线程的资源,操作系统会默认回收掉
2.int pthread_detach(pthread_t thread);
thread:想要被分离线程的线程标识符

线程安全

1.线程不安全的现象
抢票程序
2.线程不安全的原理
2.1 结论:线程不安全会导致程序结果出现二义性
2.2 举例:
a.假设现在在同一个程序当中有两个线程,线程A和线程B,并且有一个int类型的全局变
量,值为10;线程A和线程B在各自的入口函数当中都有这样的一个全局变量进行
++操作
b.线程A拥有cpu之后,对全局变量进行++操作,并且是非原子操作,也即是意味着线
程A在执行加的过程种有可能会被打断,假设,线程A刚刚将全局变量的数值10读到
cpu的寄存器当中来,就被切换出去了;程序计数器当中保存下一条执行的指令,上
下文信息当中保存寄存器的值,这两个东西是用来当线程A再次拥有cpu的时候,恢
复现场使用的
c.这会有可能线程B拥有了cpu资源,对全局变量进行了++,并且将10加成了11.回写到
内存当中了
d.线程A再次拥有cpu资源之后,恢复现场,继续往下执行,从寄存器当中读取到的值
仍然是10,加完之后为11,回写到内存当中也是11
总结:理论上,线程A和线程B各自对全局变量进行了加1操作,理论上全局变量的值应该
变成12,但是,现在程序计算的结果有可能是11,所以这就是线程不安全。
在这里插入图片描述
3.线程不安全的现象怎么解决
互斥:互斥的现象
同步: 同步的现象(为了在互斥的基础上追求资源分配的合理性)

· 互斥:同一时刻,只允许一个执行流(线程)去访问临界资源
临界资源:多个执行流都能访问到的资源,就称之为临界资源
临界区:访问临界资源的代码块

怎么保证互斥:
//互斥锁:

//线程A:
	lock
	   //临界区
	unlock
//线程B:
	lock
	   //临界区
	unlock

互斥:
1.想要保证互斥,我们需要用到互斥锁
2.互斥锁本身也是一个资源,或者说我们也需要在代码当中来获取互斥锁;只要多个线程想要保证互斥,需要都去获取互斥锁,否则就无法保证互斥

互斥锁:
1.本质:在互斥锁内部当中有一个计数器,其实就是互斥量
计数器的取值只能为0 或者为1
当线程获取互斥锁的时候,如果计数器当中的值为0,表示当前线程获取不到互斥锁,也就是没有获取互斥锁,就不要再去获取临界资源了
当线程获取互斥锁的时候,如果计数器当中的值为1,表示当前线程可以获取到互斥锁,也就是意味着可以访问临界资源,,代码可以执行临界区当中的代码
2.计数器当中的值如何保证原子性
问题:为什么计数器当中的值从0变成1,或者从1变成0,是原子操作?
在这里插入图片描述
获取锁资源的时候(加锁的时候)
1.寄存器当中的值直接赋值成为0
2.将寄存器当中的值和计数器当中的值进行交换
3.判断寄存器当中的值,得出加锁结果
在这里插入图片描述
当寄存器当中的值为1时。则表示可以加锁;
当寄存器当中的值为0时,则表示不可以加锁;

互斥锁的接口

1.初始化互斥锁变量
1.1 动态初始化
pthread_mutex_t :互斥锁变量的类型

int pthread_mutex_init(pthread_mutex_t* mutex, pthread_mutexattr_t* attr)

mutex:传入互斥锁变量的地址,pthread_mutext_init会初始化互斥锁变量
attr: 属性,一般传递NULL, 采用默认属性
eg: pthread_mutext_t lock_;
pthread_mutext_init(&lock_, NULL);

  1.2 静态初始化
  pthread_mutex_t lock_ = PTHREAD_MUTEX_INITIALIZER;
  pthread_mutex_t ==>结构体
  PTHREAD_MUTEX_INITIALIZER 宏定义了一个结构体的值
  #define  PTHREAD_MUTEX_INITIALIZER  { { 0, 0, 0, 0, 0, _PTHREAD_SPINS,  { 0, 0 } } }

2.加锁

int pthread_mutex_lock(pthread_mutex* mutex);//阻塞加锁的接口

mutex: 传入互斥锁变量的地址

如果mutex当中的计数器的值为1, 则pthread_mutex_lock接口就返回了,表示加锁成功,同时计数器当中的值会被更改为0
如果mutex当中的计数器的值为0, 则pthread_mutex_lock接口就阻塞了,pthread_mutex_lock接口没有返回,阻塞在该函数的内部,直到加锁成功

int pthread_mutex_trylock(pthread_mutex_t* mutex);

1.该接口时非阻塞加锁接口
2.含义:
2.1 当互斥锁变量当中的计数器置为1,则加锁成功返回
2.2 当互斥锁变量当中的计数器置为0, 也会返回,但是一定要清楚,加锁并没有成功,也即是不要去访问临界资源
2.3 一般我们非阻塞接口都需要搭配循环来使用
3.带有超时时间的加锁接口

int pthread_mutex_timedlock(pthread_mutex_t* mutex, const struct timespec* abs_timeout);

3.1 带有超时时间的接口,也就意味着当不能直接获取互斥锁的时候,等待abs_timeout时间,如果在这个时间内加锁成功了,直接返回,不需要再继续等待剩余的时间,并且表示加锁成功;如果超过该时间,也返回掉了,但是表示加锁失败了,需要循环加锁

3.解锁

int pthread_mutex_unlock(pthread_mutex_t* mutex)

1.不管是哪一个加锁接口加锁成功的,都可以使用该接口进行解锁
2. 解锁的时候,会将互斥锁变量当中的计数器的值,从0变为1,表示其他线程可以获取互斥锁

4.销毁互斥锁
针对的是动态初始化的互斥锁

pthread_mutext_destroy(pthread_mutext_t*);

同步&条件变量

1.同步是为了保证各个线程对临界资源访问的合理性
2.条件变量本质:PCB等待队列 + 一堆接口(等待接口 + 唤醒接口)
在这里插入图片描述
条件变量的接口
1.初始化

int pthread_cond_init(pthread_cond_t* cond, pthread_condattr_t* attr)

cond: pthread_cond_t : 条件变量的类型,传参的时候还是传入条件变量的地址
attr: 条件变量的属性,通常传递NULL,采用默认属性

2.等待
将调用该接口的线程放到PCB等待队列当中

int pthread_cond_wait(pthread_cond_t* cond, pthread_mutex_t* mutex);

cond: 条件变量
mutex: 互斥锁

3.唤醒

int pthread_cond_signal(pthread_cond_t* cond);

作用是:通知PCB等待队列当中的线程,将其从队列当中出队,唤醒该线程,唤醒至少一个PCB等待队列当中的线程

int pthread_cond_broadcast(pthread_cond_t* cond)

唤醒PCB等待队列当中全部的线程

4.释放

int pthread_cond_destory(pthread_cond_t* cond)

pthread_cond_wait接口:
1.为什么会有互斥锁
1.1 同步并没有保证互斥,而保证互斥是使用了互斥锁
1.2 pthread_cond_wait 内部会进行解锁互斥锁
问题:到底是先放到PCB等待队列,还是先解锁?
一定先放到PCB等待队列当中,再进行解锁
2.该接口内部实现逻辑
2.1 将调用pthread_cond_wait函数的执行流放到PCB等待队列当中
2.2 解互斥锁
2.3 等待被唤醒
问题:假设被唤醒之后,应该如何做?
1. 从PCB等待队列当中移除出来
2. 抢占互斥锁
情况1:拿到互斥锁, pthread_cond_wait函数就返回了
情况2: 没有抢到互斥锁,阻塞在pthread_cond_wait函数内部
抢锁逻辑当中
一定要知道,当卡在pthread_cond_wait内部枪锁逻辑的执行流一旦时间片耗尽,意味着当前当前线程被切换出来,程序计数器当中保存的就是枪锁的指令,上下文信息当中保存的就是寄存器当中的值
当再次拥有CPU时间片之后,从程序计数器和上下文信息当中恢复枪锁逻辑
直到枪锁成功,pthread_cond_wait函数才返回
在这里插入图片描述

死锁

1.当多个执行流使用同一个互斥锁的时候,有一个执行流获取到了互斥锁之后,但是没有释放互斥锁,当值其他执行流都卡死在加锁的接口当中,我们称之为死锁
gdb:
查看多线程的调用堆栈,可以使用thread apply all bt
2.
在这里插入图片描述
多个执行流,多个互斥锁的情况下,每一个执行流都又一把互斥锁,但是还要申请对方的互斥锁,这种情况下,就会导致各个指令流都阻塞掉,这种现象称之为死锁。

查看多尺线程堆栈
thread apply all bt
跳转到具体线程的堆栈当中
t [线程编号] 线程编号就是gdb调试的时候,看到的Thread[num]
跳转到具体的某一个堆栈当中去
f[堆栈编号]
在这里插入图片描述

死锁的四个必要条件

1.互斥条件
2. 请求与保持条件,吃着碗里的,看着锅里的
3. 不可剥夺条件
4. 循环等待

怎么预防死锁
1.破坏必要条件
2.加锁顺序一致
3.不要忘记解锁,在所有可能导致执行流退出的地方都进行解锁

如果说程序死锁之后,如何直接调试每一个线程
gdb attach[pid] : 将进程附加上gdb

生产者与消费者模型
123规则

一个线程安全队列 std::queue
队列的特性:先进先出,所有满足先进先出特性的结构体我们都可以称之为队列
线程安全:需要保证在同一时刻,队列当中的元素只有一个执行流去访问
互斥锁+条件变量 pthread_mutex_t pthread_cond_t

两种角色的线程
生产线程 & 消费线程

三种关系
生产者与生产者互斥
消费者与消费者互斥
生产者与消费者同步+互斥

有点:
1.支持忙闲不均
2.生产者与消费者解耦开来
3.支持高并发
在这里插入图片描述

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值