目录
一、线程概念
1.线程和进程的关系(工厂与流水线的关系)
- 线程是依附于进程才能存在的,如果没有进程,则线程不会单独存在。(没有工厂就没有流水线)
- 多线程的存在是为了提高整个程序的运行效率的。线程也被称之为执行流,因为在执行用户自己写的代码。(一个工厂可以建造多条流水线来提高效率)
2.曾经学习过的线程(你却不知道)
理解之前的:
进程在内核当中就是一个task_struct,在该结构体当中的成员变量pid被我们称之为进程号。
现在需要理解的:
>>1.操作系统当中没有线程的概念,程序员说的创建线程,本质上在linux操作系统当中创建轻量级进程(lwp),所以轻量级进程等价于线程。
>>2.曾经写的代码之中有没有线程呢?
有的!曾经写的代码当中存在线程,就是执行main函数的执行流,被称之为主线程。
注意:程序员创建的线程被称之为“工作线程”
>>3.pid本质上是轻量级进程的id,换句话说就是线程ID
在task_struct当中
pid_t pid;//轻量级进程id,也被称之为线程id,不同的线程拥有不同的pid
pid_t tgid; //轻量级进程组id,也被称之为进程id,一个进程当中的线程拥有相同的tgid
为什么进程概念的时候,说pid就是进程id?
因为主线程的pid和tgid相等。
3.linux内核是如何创建一个线程的?
4.线程的共享和独有(结合上面的图)
独有:在进程虚拟地址空间的共享区当中
调用栈,寄存器,线程ID,errno,信号屏蔽字,调度优先级
共享:文件描述符,用户id,用户组id(tgid),信号处理方式,当前进程的工作目录。
5.线程的优缺点
优点:
- 多线程的程序,拥有多个执行流,合理使用,可以提高程序的运行效率
- 多线程程序的线程切换比多进程程序快,付出的代价小(有些可以共享的数据(全局变量)就能在线程切换的时候,不进行切换)
- 可以充分发挥多核CPU并行的优势
- 计算密集型的程序,可以进行拆分,让不同的线程执行计算不一样的事情
- I/O密集型的程序,可以进型拆分,让不同的线程执行不同的I/O操作,可以不用串型运行,提高程序的运行效率。(可以一边进行I/O,一边进行计算)
缺点:
- 编写代码的难度更加高
- 代码的(稳定性)鲁棒性要求更加高
- 线程的数量并不是越多越好
- 缺乏访问控制,可能会导致程序产生二义性结果
- 一个线程崩溃,会导致整个进程退出。
二、线程控制
1.线程创建
>>1.接口:
thread:这是一个出参,获取线程标识符(地址),本质上就是线程独有空间的首地址
attr:线程的属性信息,一般写NULL,采用默认的线程属性
属性包含,调用栈的大小,分离属性,调度策略(先来先服务,分时策略,时间片轮转),调度优先级等等
start_routine:函数指针,线程执行的入口函数(线程执行起来的时候,从该函数开始运行,切记:不是从main函数开始运行)
arg:给线程入口函数传递参数;
返回值:
成功 == 0;
失败 <0;
>>2.测试入口函数的传参
>>3.结论
结论1:不要传递临时变量给线程的入口函数
结论2:如果给线程入口函数传递了一个从堆上开辟的空间,让线程自行释放。
2.线程终止
如果需要只终止某个线程而不终止整个进程,可以有三种方法:
>>1.从线程函数return。这种方法对主线程不适用,从main函数return相当于调用exit。
>>2.线程可以调用pthread_exit终止自己。
>>3.一个线程可以调用pthread_cancel终止同一个进程中的另一个进程。(main函数数也是主线程)
pthread_exit函数
功能:线程终止,谁调用谁退出
原型:void pthread_exit(void *retval);
参数:retval:线程退出时,传递给等待线程的退出信息
注意:pthread_exit或者return返回的指针所指向的内存单元必须是全局的或者是用malloc分配的,不能在线程函数的栈上分配,因为当其他线程得到这个返回指针时线程函数已经退出了,就会导致访问非法地址的问题,有可能还会导致线程的崩溃进而导致进程的崩溃。
pthread_cancel函数
功能:取消一个执行中的线程
原型:int pthread_cancel(pthread_t thread);
参数:thread:需要取消的线程的ID(线程标识符)
返回值:成功返回0;失败返回错误码
pthread_t pthread_self(void);=
功能:谁调用获取谁的线程ID
代码验证:
3.线程等待
>>1.为什么需要线程等待?
一个线程被创建出来的默认属性是joinable属性,退出的时候,依赖其他线程来回收资源(主要是退出线程使用到的共享区当中的空间)
>>2.接口
pthread_join函数
功能:等待线程的结束,是一个阻塞调用接口
原型:int pthread_join(pthread_t thread, void **retval);
参数:thread:线程标识符
retval:退出线程的退出信息
返回值:成功返回0,失败返回错误码
调用该函数的线程将挂起等待,直到id为thread的线程(等待的线程)终止。
thread线程以不同的方式终止,通过pthread_join得到的终止状态是不同的,总结如下:
第一种:线程入口函数代码执行完毕,线程退出的,就是入口函数的返回值
第二种:pthread_exit退出的,就是pthread_exit的参数
第三种:pthread_cancel退出的,就是一个宏:PTHREAD_CANCELED
4.线程分离
>>1.概念
设置线程的分离属性,一旦线程设置了分离属性,则线程退出的时候,不需要任何人回收资源。操作系统可以进行回收
>>2接口
pthread_detach函数
功能:线程分离
原型:int pthread_detach(pthread_t thread);
参数:thread:需要分离的线程ID,也是以自己分离自己,此时的参数为(pthread_self函数的返回值)
返回值:成功返回0,失败返回错误码信息
代码验证:
三、线程与安全
1.进程线程间的互斥相关背景概念
- 临界资源:多线程执行流共享的资源就叫做临界资源
- 临界区:访问临界资源的代码,就叫做临界区
- 互斥:任何时刻,互斥保证有且只有一个执行流进入临界区,访问临界区的资源,通常对临界资源起保护作用
- 原子性:不会被任何调度机制打断的操作,该操作只有两态,要么完成,要么未完成
2.什么是线程不安全?
>>1.多个线程并发或者并行运行的时候,会导致程序结果的二义性。
>>2.举例子:假设现在有两个线程,线程A和线程B,有一个CPU,两个线程同时相对全局遍历i进行++,i的初始值为10.
此时线程A和线程B都需要对全局变量A进行++,按常理说最后i的值应该变为12.
但是,假设A先执行,但是当A刚把i的值读取到自己的寄存器当中的时候,线程切换了,切换到了线程B,线程B利用CPU对i值++之后i的值变为了11,但是当我们的线程重新切换回线程A的时候,线程A的寄存器中保存的i的值还是10,所以线程A是在i=10的基础上利用CPU对i++之后,最后返回值值还是11,所以最后i的值不是12,是11.
代码验证:
四、同步与互斥
互斥
互斥要做的事情
控制线程的访问时许。当多个线程能够同时访问到临界资源的时候,有可能会导致线程执行的结果产生二义性。而互斥就是要保证多个线程在访问同一个临界资源,执行临界区代码的时候(非原子性操作(线程可以被打断)),控制访问时序。最终让一个线程单独占用临界资源执行完,再让另一个独占执行。(实现原子性)
互斥的实现:互斥锁
1.互斥锁的原理
互斥锁的本质就是0/1计数器,计数器的取值只能为0,或者1
计数器的值为1:表示当前线程可以获取到互斥锁,从而去访问临界资源
计数器的值为0:表示当前线程不可以获取到互斥锁,从而不能访问临界资源
需要理解的是:并不是说线程不获取互斥锁不能访问临界资源,而是程序员需要在代码当中用一个互斥锁,去约束多个线程。否则线程A加锁访问,线程B访问临界资源之前不加锁,那也约束不了线程B。
2.互斥锁的计数器当中如何保证原子性?
注意:++,和--,也不是原子性的。
为什么计数器当中的值从0变成1,或者从1变成0是原子性的?
直接使用寄存器当中的值和计数器内存的值进行交换,而交换时一条指令就可以完成的。
加锁的时候:寄存器当中的值设置为(0)
第一种情况:计数器的值为1,说明锁空闲,没有被线程加锁
交换情况:
第二种情况:计数器的值为0,说明锁忙碌,被其他线程加锁拿走了
交换情况:
解锁的时候:寄存器当中的值设置为(1)
计数器的值为0,需要解锁,进行一步交换。
伪代码图:
互斥锁的接口:
初始化互斥锁的两种方法:
方法一:动态分配:
int pthread_mutex_init(pthread_mutext *restrict mutex,const pthread_mutexattr_t *restrict attr);
参数:mutex:要初始化的互斥量
atter:互斥锁的状态信息,一般设置为NULL;
方法二:静态分配:
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER
互斥加锁和解锁
加锁接口jiesuo:
接口1:int pthread_mutex_lock(pthread_mutex_t *mutex);//互斥锁的阻塞加锁接口,拿不到锁就阻塞等待,直到拿到锁。
参数:mutex:传递互斥锁变量
接口2:int pthread_mutex_trylock(pthread_mutex_t*mutex);//互斥锁的非阻塞接口
作用:拿到锁就正常返回,拿锁的时候,锁是被其他线程所占有的,拿不到所也就直接返回了,需要搭配循环来进行使用,否则我们加锁失败之后我们没有判断就直接访问临界资源,就达不到互斥的目的了。
接口3:int pthread_mutex_timedlock(pthread_t*mutex,const timespec abs_timeout);带有超时时间的加锁接口
加锁的时候:锁是空闲的则直接加锁返回,锁是忙碌的,则等待,在等待范围之内,锁被其它的线程给释放了,就可以获取互斥锁,函数返回(加锁成功),超过了等待的时间范围,锁还没有被其它线程释放,该函数直接返回。(加锁失败)
解锁接口:
int pthread_mutex_unlock(pthread_mutex_t*mutex);//解锁的接口
销毁锁的接口:
int pthread_mutex_destroy(pthread_mutex_t *mutex);
//如果是动态初始化互斥锁的,需要调用销毁接口,如果是静态初始化的,就不需要销毁了
代码验证:注意:此代码存在一个问题。(我们的线程在执行完之后没有将锁释放,会产生死锁)
同步:
1.有了同步之后为什么还要有互斥?
多个线程保证了互斥,也就是保证了线程能够独占访问临界资源了。但并不是说,各个线程在访问临界资源的时候都是合理的。同步是为了保证多个线程访问临界资源的合理性,但这个合理性是建立在多个线程保持互斥的情况下。
2.创建一个场景(举例子)
吃面的场景:
吃面的人(碗里有面的时候吃面)
做面的人(碗里没面的时候做面)
碗(0代表没面,1代表有面)注意:只有这两种场景才是合理的。
假设我们在仅仅只保证互斥的情况下:我们虽然保证了各个线程运行的独立性,避免了 合理性 ,但是结果确实不合理的。
不合理情况:碗里已经没有面了,为吃面的线程却还在吃
碗里还有面,但是做面的线程却还在做面
修改一:我们加上if条件来进行判断
存在的问题:非常的消耗CPU资源,因为判断了是否资源准备好,如果没有准备好的话,即使释放了互斥锁,下次互斥锁也可能被该线程再次拿到。
那么怎么修改呢?这就需要进行同步的操作了。
3.条件变量
>>1.条件变量怎么使用
线程在加锁后,判断下临界资源是否可用;
如果可用,则直接访问临界资源
如果不可用:则调用等待的接口,让该线程进行等待
>>2.条件变量的原理
本质上是:PCB等待队列(存放等待的线程的PCB)
4.条件变量的接口
>>1.初始化接口
int pthread_cond_init(pthread_cond_t *cond,const pthread_condatter_t attr);
参数:cond 接受一个条件变量的指针或者地址
attr:表示条件变量的属性信息,传递NULL,使用默认的属性
>>2.等待接口
int pthread_cond_wait(pthread_cond_t*cond,pthread_mutex_t*mutex);//谁(线程)调用等待接口,就将谁放到条件变量对应的PCB等待队列当中。
参数:cond,条件变量
mutex,互斥锁
>>3.唤醒接口
int pthread_cond_broadcast(pthread_cond_t*cond);//唤醒PCB等待队列当中的所有线程
int pthread_cond_signal(pthread_cont_t*cond);//唤醒PCB等待队列当中至少一个线程。
>>4.销毁接口
int pthread_cond_destroy(pthread_cond_t *cond);//条件变量的销毁
5.条件变量的代码
第二次修改:
修改三:上面的代码对于多个吃面和做面的线程而言就出现了问题: (有可能PCB等待队列中唤醒的线程不是我们想要的例如:我们先执行的是做面线程,然后我们需要唤醒的是等待队列当中的吃面线程,但是却唤醒了做面线程)
最终版本:
吃面人有自己的条件变量,做面人有自己的条件变量
吃面人发现没有面了,就将自己放到吃面人的条件变量对应的PCB等待队列当中,吃面人吃了一碗面,则通知做面人的条件变量的PCB等待队列
做面人发现碗里有面,就将自己放到做面人的条件变量对应的PCB等待队列当中,做面人做了一碗面,则 通知吃面人的条件变量的等待队列。
1 #include<stdio.h>
2 #include<unistd.h>
3 #include<pthread.h>
4
5 #define PTHREAD_COUNT 2//控制吃面人和做面人的数量
6
7 /*
8 * 第一步:创建吃面和做面的线程
9 * 第二步:我们仅仅只实现两个线程的互斥------观察现象
10 * 第三步:我们加上同步的条件
11 * */
12 int g_bowl;//创建一个碗
13 pthread_mutex_t g_lock;//创建一个锁
14 pthread_cond_t g_cond1;//创建一个吃面条件变量
15 pthread_cond_t g_cond2;//创建一个做面的条件变量
16
17 //吃面的线程
W> 18 void*pthread_eat_start(void*arg)
19 {
20 while(1)
21 {
22 //先获取锁
23 pthread_mutex_lock(&g_lock);
24 while(g_bowl==0)
25 {
26 //碗里没有面
27 printf("我是吃面的线程,但是现在碗里没有面,所以我就不吃了...\n");
28 pthread_cond_wait(&g_cond1,&g_lock);
29 }
30 printf("i am eat thread %d\n",g_bowl--);
31 //释放锁
32 pthread_mutex_unlock(&g_lock);
33 pthread_cond_signal(&g_cond2);//唤醒PCB等待队列
34 }
35
36 }
37 //做面的线程
W> 38 void*pthread_make_start(void*arg)
39 {
40 while(1)
41 {
42 //先获取锁
43 pthread_mutex_lock(&g_lock);
44 while(g_bowl==1)
45 {
46 //此时碗里有面
47 printf("我是做面的线程,但是现在碗里有面,我就不做了...\n");
48 pthread_cond_wait(&g_cond2,&g_lock);
49 }
50 printf("i am make thread %d\n",g_bowl++);
51 //释放锁
52 pthread_mutex_unlock(&g_lock);
53 pthread_cond_signal(&g_cond1);
54 }
55 }
56
57
58 int main()
59 {
60 //1.锁的初始化
61 pthread_mutex_init(&g_lock,NULL);
62 pthread_cond_init(&g_cond1,NULL);
63 pthread_cond_init(&g_cond2,NULL);
64 //2.做面吃面线程的创建
65 pthread_t eat[PTHREAD_COUNT];
66 pthread_t make[PTHREAD_COUNT];
67 for(int i = 0;i<PTHREAD_COUNT;i++)
68 {
69 //做面线程的创建
70 int ret = pthread_create(&make[i],NULL,pthread_make_start,NULL);
71 if(ret<0)
72 {
73 perror("pthread_create");
74 return 0;
75 }
76
77 //吃面线程的创建
78 ret = pthread_create(&eat[i],NULL,pthread_eat_start,NULL);
79 if(ret<0)
80 {
81 perror("pthread_create");
82 return 0;
83 }
84 }
85 //3.主线程不退出(线程等待)
86 for(int i = 0;i<PTHREAD_COUNT;i++)
87 {
88 pthread_join(make[i],NULL);
89 pthread_join(eat[i],NULL);
90 }
91
92 //4.锁的销毁
93 pthread_mutex_destroy(&g_lock);
94 pthread_cond_destroy(&g_cond1);
95 pthread_cond_destroy(&g_cond2);
96 return 0;
97 }
6.条件变量的夺命追问
>>1.条件变量的等待接口第二个参数为什么会有互斥锁?
因为在等待的接口中需要将锁进行释放,如果不进行释放的话其它的进程永远无法获得锁。
>>2.pthread_cond_wait的内部是针对互斥锁做了什么操作?先释放互斥锁还是先将线程放入到PCB等待队列
对互斥锁进行了释放,先放入等待队列再释放互斥锁,原因:如果先释放互斥锁,则有可能我们在唤醒PCB的队列的时候,该线程还没有被加载到等待队列当中。
>>3.线程被唤醒之后会执行什么代码?需要再获取互斥锁吗?
线程被唤醒之后会再次获取互斥锁,因为需要保证线程的安全性(互斥)。
pthread_cond_wait函数在返回之前一定会在其内部进行加锁操作:
抢锁的时候:
1.抢到了,pthread_cond_wait函数就真正执行完毕了,函数返回。
2.没抢到,pthread_cond_wait函数的代买就没有真正的执行完毕,还处于函数内部抢锁的逻辑当中,还会继续去抢锁的逻辑当中还会继续抢锁,直到抢到互斥锁,才返回。
五、死锁
1.死锁的概念
死锁是指在一组进程中的各个进程均占有不会释放的资源,但因互相申请被其他进程所站用不会释放的资源而处于的一种永久等待状态
2.死锁的四个必要条件
- 互斥条件:一个互斥锁,在同一时间只能被一个线程所拥有
- 请求与保持条件:一个执行流因请求资源而阻塞时,对已获得的资源保持不放
- 不可剥夺条件:一个线程获取到互斥锁之后,除了自己释放,其他线程不能进行释放的
- 循环等待条件:若干个线程之间形成一种头尾相接的循环等待资源的关系
3.死锁的场景及调试
3.1死锁的场景
第一种场景:线程加锁之后,并没有释放互斥锁
第二种场景:两种线程分别拿着一把锁,还行请求对方的锁,从而形成了一个循环,产生死锁。
3.2死锁的gdb调试:
gdb attack + 进程号
thread apply all bt:所有线程都展示调用堆栈
t + [n] :跳转到n线程中
p + g_lock1:查看1锁被哪个线程所拿
4.避免死锁
- 破坏死锁的四个必要条件:循环等待,请求与保持
- 加锁顺序一致,都先加锁1,再加锁2
- 避免锁释放的场景:在所有可能线程退出的地方都进行线程解锁
- 资源一次性分配:多个资源在代码当中又可能每一个资源都需要使用不同的锁进行保护
六、生产者与消费者模型
0.123规则
一个线程安全的队列(保证互斥和同步)
两种角色的线程:生产者 & 消费者
三个规则:
生产者与生产者互斥
消费者和消费者互斥
生产者与消费者互斥 + 同步
1.为何要使用生产者与消费者模型
生产者消费者模式就是 通过一个容器来解决生产者和消费者的强耦合问题 。生产者和消费者彼此之间不直接通讯,而通过阻塞队列来进行通讯,所以生产者生产完数据之后不用等待消费者处理,直接扔给阻塞队列,消费者不找生 产者要数据,而是直接从阻塞队列里取,阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力。这个 阻塞队列就是用来给生产者和消费者解耦的。
2.优点
解耦,忙先不均,支持高并发
3.基于BlockingQueue的生产者消费者模型
在多线程编程中阻塞队列 (Blocking Queue) 是一种常用于实现生产者和消费者模型的数据结构。其与普通的队列区别在于,当队列为空时,从队列获取元素的操作将会被阻塞,直到队列中被放入了元素;当队列满时,往队列里存 放元素的操作也会被阻塞,直到有元素被从队列中取出 ( 以上的操作都是基于不同的线程来说的,线程在对阻塞队 列进程操作时会被阻塞
4.代码验证
1 #include<stdio.h>
2 #include<pthread.h>
3 #include<unistd.h>
4 #include<queue>
5 using namespace std;
6 /*创建线程安全队列*/
7
8 #define THREAD_COUNT 2
9
10
11 class RingQueue
12 {
13 public:
14 RingQueue(int n = 10)
15 :capacity_(n)
16 {
17 pthread_mutex_init(&lock_,NULL);
18 pthread_cond_init(&prod_cond_,NULL);
19 pthread_cond_init(&cons_cond_,NULL);
20 }
21
22 //提供给生产者模型的接口
23 void Push(const int data)
24 {
25 pthread_mutex_lock(&lock_);
26 while(que_.size()>=capacity_)
27 {
28 //队列已经满了,生产停止,等待
29 pthread_cond_wait(&prod_cond_,&lock_);
30 }
31 que_.push(data);
W> 32 printf("i am product thread : %p, i product %d\n", pthread_self(), data);
33 pthread_mutex_unlock(&lock_);
34 //唤醒生消费者等待队列
35 pthread_cond_signal(&cons_cond_);
36 }
37
38 //提供给消费者模型的接口
39 void Pop(int*data)
40 {
41 pthread_mutex_lock(&lock_);
42 while(que_.size()<=0)
43 {
44 //队列已经空了,不能再拿了
45 pthread_cond_wait(&cons_cond_,&lock_);
46 }
47 *data = que_.front();
48 que_.pop();
W> 49 printf("i am consum thread : %p, i consum %d\n", pthread_self(), *data);
50 pthread_mutex_unlock(&lock_);
51 pthread_cond_signal(&prod_cond_);
52 }
53 ~RingQueue()
54 {
55 pthread_mutex_destroy(&lock_);
56 pthread_cond_destroy(&prod_cond_);
57 pthread_cond_destroy(&cons_cond_);
58 }
59 private:
60 queue<int>que_;
61
62 //互斥
63 pthread_mutex_t lock_;
64 //同步
65 pthread_cond_t prod_cond_;
66 pthread_cond_t cons_cond_;
67
68 size_t capacity_;
69 };
70 void*cons_start(void*arg)
71 {
72 RingQueue*rq = (RingQueue*)arg;
73 while(1)
74 {
75 int data;
76 rq->Pop(&data);
77 }
78 }
79
80 int g_data = 0;
81 pthread_mutex_t g_lock = PTHREAD_MUTEX_INITIALIZER;
82
83 void* prod_start(void*arg)
84 {
85 RingQueue*rq = (RingQueue*)arg;
86 while(1)
87 {
88 pthread_mutex_lock(&g_lock);
89 rq->Push(g_data);
90 g_data++;
91 sleep(1);
92 pthread_mutex_unlock(&g_lock);
93 }
94 }
95
96
97
98 int main()
99 {
100 RingQueue*rq = new RingQueue();
101 //创建消费者和生产者线程
102 pthread_t prod[THREAD_COUNT];
103 pthread_t cons[THREAD_COUNT];
104
105 for(int i = 0 ;i<THREAD_COUNT;i++)
106 {
107 int ret = pthread_create(&prod[i],NULL,prod_start,(void*)rq);
108 if(ret<0)
109 {
110 perror("pthread_create");
111 return 0;
112 }
113 ret = pthread_create(&cons[i],NULL,cons_start,(void*)rq);
114 if(ret<0)
115 {
116 perror("pthread_create");
117 return 0;
118 }
119 }
120
121 //主线程不退出
122 for(int i = 0 ;i<THREAD_COUNT;i++)
123 {
124 pthread_join(prod[i],NULL);
125 pthread_join(cons[i],NULL);
126 }
127 delete rq;
128 return 0;
129 }