Linux多线程(中)
1.线程安全
什么是线程不安全?
多个线程并发执行一段代码时,会导致程序结果的二义性
假设有两个线程A,B,有一个CPU,两个线程想同时对全局变量i=10进行加加,如果A从内存中读到i的值后还没有加加,线程就切换了,此时切换成线程B从内存中读到了i并加加为11,B加完后轮到线程A,线程A之前读到的值为10加完后i的值仍然是11;另一种情况则是A加完后B再加,此时的到值就是12。因此产生了结果二义性的问题。
下面是一个不安全线程的代码:
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
int g_ticket = 100000;
void* my_thread_start(void* arg){
//修改全局变量
while(g_ticket > 0){
printf("i am %p, cout g_i val is %d\n", pthread_self(), g_ticket);
g_ticket--;
}
return NULL;
}
int main(){
//1.创建线程
// 两个工作线程修改全局变量
pthread_t tid[2];
int i;
for(i = 0; i < 2; i++){
int ret = pthread_create(&tid[i], NULL, my_thread_start, NULL);
if(ret < 0){
perror("pthread_create");
return 0;
}
}
//2. 主线程 (只要不退出就好)
for( i = 0; i < 2; i++){
pthread_join(tid[i], NULL);
}
return 0;
}
那么什么是线程安全?
- 多个线程并发同一段代码时,不会出现不同的结果。常见对全局变量或者静态变量进行操作,并且没有锁保护的情况下,会出现该问题。
- 每个线程对全局变量或者静态变量只有读取的权限,而没有写入的权限,一般来说这些线程是安全的
- 类或者接口对于线程来说都是原子操作
- 多个线程之间的切换不会导致该接口的执行结果存在二义性
那么该如何解决线程不安全的问题呢?可以通过下面的三点来解决。
代码必须要有互斥行为:当代码进入临界区执行时,不允许其他线程进入该临界区。
如果多个线程同时要求执行临界区的代码,并且临界区没有线程在执行,那么只能允许一个线程进入该临界区。
如果线程不在临界区中执行,那么该线程不能阻止其他线程进入临界区。
而要做到这三点,本质上就是需要一把锁,即互斥锁
2.同步与互斥
进程线程间的互斥相关背景概念:
- 临界资源:多线程执行流共享的资源就叫做临界资源,如上代码中的:
- 临界区:每个线程内部,访问临界资源的代码,就叫做临界区,如上代码中的:
- 互斥:任何时刻,互斥保证有且只有一个执行流进入临界区,访问临界资源,通常对临界资源起保护作用
- 原子性:不会被任何调度机制打断的操作,该操作只有两态,要么完成,要么未完成
1.互斥锁
原理:
互斥锁的本质是0/1计数器(互斥量),计数器的取值只能为0/1
计数器的值为1:表示当前线程可以获取到互斥锁,从而去访问临界资源
计数器的值为0:表示当前线程不可以获取到互斥锁,从而不能访问临界资源
注意:并不是说线程不获取互斥锁就不能访问临界资源,而是程序员需要在代码中使用一个互斥锁,去约束多个进程。
为什么计数器中的值01变化是原子性的呢?
直接使用寄存器当中的值和计数器内存的值交换,而交换是一条汇编指令就可以完成的(底层的操作一步完成)
加锁的时候:寄存器当中的值设置为(0)
第一种情况:计数器的值为1,说明锁空闲,没有被线程加锁,就可以加锁成功
第二种情况:计数器的值为0,说明锁忙碌,被其他线程加锁拿走了
解锁的时候,寄存器当中的值设置为(1),计数器的值为0,需要解锁,进行一步交换
接口:
动态初始化:int pthread_mutex_init(pthread_mutex_t* mutex,const pthread_mutexattr_t* attr);
静态初始化:pthread_mutex_t mutex=PTHREAD_MUTEX_INITIALIXER;
#define PTHREAD_MUTEX_INITIALIXER {{0,0,0,0,0…}}
加锁:int pthread_mutex_lock(pthread_mutex_t *mutex);
代码实现:
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
int g_ticket = 100;
pthread_mutex_t g_lock;
void* my_thread_start(void* arg){
//修改全局变量
pthread_mutex_lock(&g_lock);
while(g_ticket > 0){
printf("i am %p, cout g_i val is %d\n", pthread_self(), g_ticket);
g_ticket--;
}
return NULL;
}
int main(){
//0.初始化互斥锁
pthread_mutex_init(&g_lock, NULL);
//1.创建线程
// 两个工作线程修改全局变量
pthread_t tid[2];
for(int i = 0; i < 2; i++){
int ret = pthread_create(&tid[i], NULL, my_thread_start, NULL);
if(ret < 0){
perror("pthread_create");
return 0;
}
}
//2. 主线程 (只要不退出就好)
for(int i = 0; i < 2; i++){
pthread_join(tid[i], NULL);
}
return 0;
}
解锁接口:
int pthread_mutex_unlock(pthread_mutex_t *mutex);
mutex:传递互斥锁变量
注意:在线程所有可能退出的地方都进行解锁!否则可能导致死锁(退出的线程将互斥锁拿走了,其他等待线程永远不可能拿到互斥锁了)
销毁接口:
int pthread_mutex_destroy(pthread_mutex_t *mutex);
如果是动态初始化互斥锁的,需要调用销毁接口。如果是静态初始化互斥锁的,就不需要销毁了
代码实现:
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
int g_ticket = 1000;
pthread_mutex_t g_lock;
void* my_thread_start(void* arg){
//修改全局变量
while(1){
pthread_mutex_lock(&g_lock);
if(g_ticket <= 0){
pthread_mutex_unlock(&g_lock);
break;
}
printf("i am %p, cout g_i val is %d\n", pthread_self(), g_ticket);
g_ticket--;
pthread_mutex_unlock(&g_lock);
}
return NULL;
}
int main(){
//0.初始化互斥锁
pthread_mutex_init(&g_lock, NULL);
//1.创建线程
// 两个工作线程修改全局变量
pthread_t tid[2];
for(int i = 0; i < 2; i++){
int ret = pthread_create(&tid[i], NULL, my_thread_start, NULL);
if(ret < 0){
perror("pthread_create");
return 0;
}
}
//2. 主线程 (只要不退出就好)
for(int i = 0; i < 2; i++){
pthread_join(tid[i], NULL);
}
//3.释放互斥锁
pthread_mutex_destroy(&g_lock);
return 0;
}
2.同步
有了互斥之后为什么还要有同步?
多个线程保证了互斥,也就是保证了线程能够独占访问临界资源,但并不是说,各个线程在访问临界资源的时候都是合理的
而同步出现了,为了保证多个线程对临界资源访问的合理性,这个合理性建立在保证互斥的情况下
什么是条件变量?
当一个线程互斥地访问某个变量时,它可能发现在其它线程改变状态之前,它什么也做不了。
例如一个线程访问队列时,发现队列为空,它只能等待,只到其它线程将一个节点添加到队列中。这种情况就需要用到条件变量。
- 使用原理:线程在加锁后,判断下临界资源是否可用
如果可用,则直接访问临界资源;如果不可用,则调用等待接口,让该线程进行等待- 原理:本质上是PCB等待队列(存放在等待的线程的PCB)
条件变量的接口:
- 初始化接口:int pthread_cond_init(pthread_cond_t *restrict cond,const pthread_condattr_t *restrict attr);
pthread_con_t:条件变量类型
参数:cond,接受一个条件变量的指针,接受一个条件变量的地址
attr:表示条件变量的属性信息,传递NULL,采用默认属性
静态初始化:pthread_cond_t cond=PTREAD_COND_INITALIZER;- 等待接口:int pthread_cond_wait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex);
参数:cond,条件变量
mutex:互斥锁- 唤醒接口:int pthread_cond_broadcast(pthread_cond_t *cong);//唤醒PCB等待队列当中的所有线程
int pthread_cond_signal(pthread_cond_t *cond)j,//唤醒PCB等待都一列当中至少一个线程- 销毁接口:int pthread_cond_destroy(pthread_cond_t *cond);条件变量的销毁
代码实现(建立做面与吃面的线程):
#include<stdio.h>
#include<unistd.h>
#include<pthread.h>
#define THREAD_COUNT 2
//代表碗里的情况:0表示没面,1表示有面
int g_bowl=0;
pthread_mutex_t g_lock;
pthread_cond_t g_eat_cond;
pthread_cond_t g_make_cond;
//工作线程
void* eat_thread_start(void* arg){
while(1){
pthread_mutex_lock(&g_lock);
//没面等待
while(g_bowl==0){
pthread_cond_wait(&g_eat_cond,&g_lock);
}
printf("我是 %p,碗里有面,我可以吃:%d\n",pthread_self(),g_bowl--);
pthread_mutex_unlock(&g_lock);
//通知做面的人做面
pthread_cond_signal(&g_make_cond);
}
}
//工作线程
void* make_thread_start(void* arg){
while(1){
pthread_mutex_lock(&g_lock);
//有面等待
while(g_bowl==1){
pthread_cond_wait(&g_make_cond,&g_lock);
}
printf("我是%p,碗里没有面,我可以做面了,%d\n",pthread_self(),g_bowl++);
pthread_mutex_unlock(&g_lock);
pthread_cond_signal(&g_eat_cond);
}
}
int main(){
//1。初始化互斥锁
pthread_mutex_init(&g_lock,NULL);
pthread_cond_init(&g_eat_cond,NULL);
pthread_cond_init(&g_make_cond,NULL);
//2.创建吃面的线程和做面的线程
pthread_t eat[THREAD_COUNT],make[THREAD_COUNT];
int i;
for(i=0;i<THREAD_COUNT;i++){
int ret=pthread_create(&eat[i],NULL,eat_thread_start,NULL);
if(ret<0){
perror("pthread_create");
return 0;
}
ret=pthread_create(&make[i],NULL,make_thread_start,NULL);
if(ret<0){
perror("pthread_create");
return 0;
}
}
//3.等待两种线程
for(i=0;i<THREAD_COUNT;i++){
pthread_join(eat[i],NULL);
pthread_join(make[i],NULL);
}
//4.销毁互斥锁
pthread_mutex_destroy(&g_lock);
pthread_cond_destroy(&g_eat_cond);
pthread_cond_destroy(&g_make_cond);
return 0;
}
深入了解:
- 条件变量的的等待接口第二个参数为什么会有互斥锁?
等待接口谁调用,就将谁放到条件变量对应的PCB等待队列中;
一是为了在线程访问临界资源之前,一定是加锁访问的,保证互斥属性
二是传递给pthread_cond_wait接口,就是想让进行解锁
- pthread_cond_wait的内部是针对互斥锁做了什么操作?先释放互斥锁还是将线程放入到PCB等待队列
先放到pcb等待队列在进行解锁
- 线程被唤醒之后会执行什么代码,需要在获取互斥锁吗?
被唤醒之后,线程再次判断有无临界资源可以访问,有就访问,没有就继续等待
4.死锁
产生场景:
1.线程加锁之后,并没有释放互斥锁
2.两种线程分别拿着一把锁,还想请求对方的锁
代码举例:
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
#define THREAD_COUNT 1
//代表碗: 0表示没有面, 1表示有面
int g_bowl = 0;
pthread_mutex_t g_lock1;
pthread_mutex_t g_lock2;
void* eat_thread_start(void* arg){
pthread_mutex_lock(&g_lock2);
sleep(1);
pthread_mutex_lock(&g_lock1);
pthread_mutex_unlock(&g_lock1);
pthread_mutex_unlock(&g_lock2);
}
void* make_thread_start(void* arg){
pthread_mutex_lock(&g_lock1);
sleep(1);
pthread_mutex_lock(&g_lock2);
pthread_mutex_unlock(&g_lock2);
pthread_mutex_unlock(&g_lock1);
}
int main(){
//1.初始化互斥锁
pthread_mutex_init(&g_lock1, NULL);
pthread_mutex_init(&g_lock2, NULL);
//2.创建吃面的线程 和 做面的线程
pthread_t eat[THREAD_COUNT], make[THREAD_COUNT];
for(int i = 0; i < THREAD_COUNT; i++){
int ret = pthread_create(&eat[i], NULL, eat_thread_start, NULL);
if(ret < 0){
perror("pthread_create");
return 0;
}
ret = pthread_create(&make[i], NULL, make_thread_start, NULL);
if(ret < 0){
perror("pthread_create");
return 0;
}
}
//3.等待两种线程
for(int i = 0; i < THREAD_COUNT; i++){
pthread_join(eat[i], NULL);
pthread_join(make[i], NULL);
}
//4.销毁互斥锁
pthread_mutex_destroy(&g_lock1);
pthread_mutex_destroy(&g_lock2);
return 0;
}
死锁的必要条件:
- 不可剥夺:线程获取到互斥锁之后,除了自己释放,其他线程不能进行释放
- 循环等待:线程A拿着1锁请求2锁,线程B拿着2锁去请求1锁
- 互斥条件:一个互斥锁在同一时间只能被一个线程所有
- 请求与保持:吃着碗里的,看着锅里的
代码如何避免死锁?
破坏必要条件:循环等待 请求与保持
加锁顺序一致,都先加1锁,再加2锁
避免锁没有被释放:在所有可能线程退出的地方都进行解锁
资源一次性分配:多个资源在代码中有可能每一个资源都需要使用不同的锁进行保护