1. 多线程概念
1.1 什么是线程
- linux内核中是没有线程的概念的,而是轻量级进程的概念:LWP,通俗的线程概念其实是c库当中的概念。
- 在一个程序里的一个执行路线就叫线程(thread),即线程是一个进程内部的控制序列
- 线程在进程内部运行,本质是在进程地址空间内运行,一切进程至少都有一个执行线程
从内核角度分析:
创建子进程:
- fork:在内核当中以父进程的PCB为模板拷贝父进程PCB当中的内容创建一个PCB,PCB被内核使用双向链表管理起来
- vfork:在内核当中以父进程的PCB为模板拷贝父进程PCB部分内容创建一个PCB,子进程的PCB是指向父进程的虚拟地址空间的,PCB被内核使用双向链表管理起来
对于调用堆栈混乱的问题:是让子进程先运行,然后父进程才运行的。
创建线程:
-
pthread_create:在内核中创建一个PCB,PCB是指向父进程的虚拟地址空间的,内核会给创建出来的线程,在虚拟地址空间当中的共享区开辟一段空间,保存线程独有的东西
-
pid_t pid:轻量级进程id
-
pid_t tgid:轻量级进程组id
-
当当前程序只有一个执行流的时候,意味着当前程序只有一个线程,将执行main函数的线程,称之为主线程,即tgid(线程组id(进程id))==主线程id(pid)
-
当程序有多个执行流的时候,意味着当前程序有多个线程。
- 主线程:执行main函数,pid == tgid
- 工作线程:新创建出来的执行流
tgid:相当于进程号(线程组id),是一定不会变的,标识当前的线程属于哪一个进程
pid:相当于线程id,每一个线程id都是不同的。
-
2. 线程控制
2.1 创建线程
函数:
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,void *(*start_routine) (void *), void *arg)
函数说明:
- pthread_t:线程标识符,是一个出参
- pthread_attr_t:线程属性,一般传值为NULL,采用默认属性,线程在创建出来时,属性中默认是joinable属性(意味着线程在退出的时候需要其他执行流来回收线程的资源)
- start_routine:本质是函数指针,保存线程入口函数的地址
- arg:给线程入口函数的传参
- 返回值:创建失败,小于0;创建成功,等于0
代码示例:
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
//线程入口函数
void* Tstart(void* arg)
{
while(1)
{
printf("i am work thread \n");
sleep(1);
}
return NULL;
}
int main()
{
pthread_t tid; //线程标识符
int ret = pthread_create(&tid,NULL,Tstart,NULL);
if(ret < 0)
{
perror("pthread");
}
while(1)
{
printf("i am main thread \n");
sleep(1);
}
return 0;
}
运行以后使用pstack查看堆栈调用信息:
使用top命令查看进程动态
- top之后,按 1 可以查看各个CPU的负载
- top -H -p [pid]:查看各个线程的工作状态
使用该函数要注意传参问题:
代码示例:
#include <stdio.h>
#include <sys/types.h>
#include <fcntl.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
struct Data
{
int data;
};
void *tStart(void *arg)
{
// int *i = (int*)arg;
Data *d = (Data*)arg;
while(1)
{
//pthread_self() 获取自己线程的标识符
printf("I am workthread %p i = %d\n",pthread_self(),d->data);
sleep(1);
}
//释放空间
delete d;
}
int main()
{
pthread_t tid;
for(int i = 0;i<4;i++)
{
//传临时变量
// int ret = pthread_create(&tid,NULL,tStart,&i);
// 传结构体对象
#if 0
struct Data d;
d.data = i;
int ret = pthread_create(&tid,NULL,tStart,(void*)&d);
#endif
//传结构体指针
//指针变量也是在栈上开辟空间的 new是在堆上开辟的空间
Data *d = new Data();
d->data = i;
int ret = pthread_create(&tid,NULL,tStart,(void*)d);
if(ret<0)
{
perror("pthread");
return -1;
}
}
while(1)
{
printf("I am mainthread\n");
sleep(1);
}
return 0;
}
-
传递临时变量:
- 临时变量的生命周期
- 临时变量的值的改变
- 传递临时变量有可能导致越界的问题
-
结构体对象:参考临时变量
-
结构体指针:
总之一句话,不推荐传递临时变量。
2.2 线程终止
2.2.1 return返回
从入口函数的return返回,该线程就退出掉了
2.2.2 void pthread_exit(void *retval)
函数说明:
- retval:返回信息,可以给也可以不给,是返回给等待线程退出的执行流的,如果不给,则传递NULL
- 谁调用谁退出。
代码示例:
- 让工作线程调用
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
void* Tstart(void* arg)
{
(void) arg;
sleep(10); //工作线程休眠10s
printf("i am work thread \n");
pthread_exit(NULL);
printf("i am flag\n");
return NULL;
}
int main()
{
pthread_t tid;
int ret = pthread_create(&tid,NULL,Tstart,NULL);
if(ret < 0)
{
perror("pthread");
}
while(1)
{
printf("i am main thread \n");
sleep(1);
}
return 0;
}
运行结果:
可以看到调用pthread_exit函数,工作线程是立即退出的
- 让主线程调用
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
void* Tstart(void* arg)
{
(void)arg;
while(1)
{
printf("i am workthread \n");
sleep(1);
}
}
int main()
{
pthread_t tid;
int ret = pthread_create(&tid,NULL,Tstart,NULL);
if(ret < 0)
{
perror("pthread");
}
//主线程调用
pthread_exit(NULL);
while(1)
{
printf("i am main thread \n");
sleep(1);
}
return 0;
}
运行结果:
可以看到主线程成为了僵尸进程,工作线程一直运行
2.2.3 int pthread_cancel(pthread_t thread)
函数说明:
- thread:线程标识符
- 调用该函数的执行流可以取消其他线程,但是需要知道其他线程等待线程标识符,也可以执行流自己取消自己,传入自己的线程标识符
- 获取自己的线程标识符:pthread_self()
代码示例:
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
void* Tstart(void* arg)
{
(void) arg;
sleep(20);
printf("i am work thread \n");
pthread_cancel(pthread_self());
//sleep(5);
printf("cancel\n");
return NULL;
}
int main()
{
pthread_t tid;
int ret = pthread_create(&tid,NULL,Tstart,NULL);
if(ret < 0)
{
perror("pthread");
}
while(1)
{
printf("i am main thread \n");
sleep(1);
}
return 0;
}
运行结果:
可以看到,调用pthread_cancel并不会让线程立即退出,但是最终线程也被终止了,在调用pthread_cancel后,休眠5s,发现没有打印直接退出。
2.3 线程等待
函数:
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函数当中的,即谁调用谁阻塞,直到等待的线程退出,才返回。
代码示例:
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#define THREADCOUNT 1
void* Tstart(void* arg)
{
(void)arg;
sleep(10); //等待10s退出
printf("i am workthread \n");
pthread_exit(NULL);
return NULL;
}
int main()
{
pthread_t tid[THREADCOUNT];
int i = 0;
//线程创建
for(;i<THREADCOUNT;i++)
{
int ret = pthread_create(&tid[i],NULL,Tstart,NULL);
if(ret < 0)
{
perror("pthread_create");
return -1;
}
}
//线程等待
for(i=0;i<THREADCOUNT;i++)
{
pthread_join(tid[i],NULL);
}
while(1)
{
printf("i am main thread \n");
sleep(1);
}
return 0;
}
运行结果:
2.4 线程分离
函数:
int pthread_detach(pthread_t thread)
函数说明:
- thread:想要被分离线程的线程标识符
- 作用:改变线程的属性,将joinable属性改变为detach属性,当线程退出的时候,不需要其他线程再来回收退出线程的资源,操作系统会默认回收掉。
使用:
可以在主线程中分离创建出来的线程,也可以在工作线程的入口函数中自己分离
3. 线程安全
3.1 概念
多个线程并发同一段代码时,不会出现不同的结果。常见对全局变量或者静态变量进行操作,并且没有锁保护的情况下,会出现该问题。
3.1.1 常见线程不安全的情况
- 不保护共享变量的函数
- 函数状态随着被调用,状态发生变化的函数
- 返回指向静态变量指针的函数
- 调用线程不安全函数的函数
3.1.2 常见线程安全的情况
- 每个线程对全局变量或者静态变量只有读取的权限,而没有写入的权限,一般来说这些线程是安全的
- 类或者接口对于线程来说都是原子操作
- 多个线程之间的切换不会导致该接口的执行结果存在二义性
3.2 线程互斥
3.2.1 概念
- 临界资源:多线程执行流共享的资源就叫做临界资源
- 临界区:每个线程内部,访问临界资源的代码,就叫做临界区
- 互斥:任何时刻,互斥保证有且只有一个执行流进入临界区,访问临界资源,通常对临界资源起保护作用
- 原子性:不会被任何调度机制打断的操作,该操作只有两态,要么完成,要么未完成
3.2.2 互斥量
在代码中++或–操作并不是原子操作,而是对应三条汇编指令:
- load:将共享变量从内存加载到寄存器中
- update: 更新寄存器里面的值,执行-1或+1操作
- store:将新值,从寄存器写回共享变量的内存地址
要解决以上问题,需要做到三点:
- 代码必须要有互斥行为:当代码进入临界区执行时,不允许其他线程进入该临界区。
- 如果多个线程同时要求执行临界区的代码,并且临界区没有线程在执行,那么只能允许一个线程进入该临界区。
- 如果线程不在临界区中执行,那么该线程不能阻止其他线程进入临界区。
互斥锁:
-
本质:在互斥锁内部当中有一个计数器,其实就是互斥量
计数器的取值只能为0或者1- 当线程获取互斥锁的时候,如果计数器当中的值为0,表示当前线程获取不到互斥锁,也就是没有获取互斥锁,就不要再去获取临界资源了。
- 当线程获取互斥锁的时候,如果计数器当中的值为1,表示当前线程可以获取到互斥锁,也就是意味着可以访问临界资源,代码可以执行临界区中的代码
计数器当中的值如何保证原子性:
为什么说计数器当中的值从0变成1,或者从1变成0,是原子操作,是因为在获取锁资源的时候(加锁的时候):
- 寄存器当中的值直接赋值成为0
- 将寄存器当中的值和计数器当中的值进行交换
- 判断寄存器当中的值,得出加锁结果
第一种情况:
第二种情况:
当寄存器当中的值为1时,则表示可以加锁
当寄存器当中的值为0时,则表示不可以加锁
3.3 互斥锁的接口
3.3.1 初始化互斥锁变量
动态初始化:
-
函数:
int pthread_mutex_init(pthread_mutex_t *restrict mutex,const pthread_mutexattr_t *restrict attr) -
函数说明:
- pthread_mutex_t:互斥锁变量类型
- mutex:传入互斥锁变量的地址,pthread_mutex_init会初始化互斥锁变量
- attr:属性,一般传递NULL,采用默认属性
eg:pthread_mutex_t lock;
pthread_mutex_init(&lock,NULL);
静态初始化:
-
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 } } }
3.3.2 加锁 (-1)
函数1:
- int pthread_mutex_lock(pthread_mutex_t *mutex)
函数说明:
-
会阻塞加锁的接口
-
mutex:传入互斥锁变量的地址
-
含义:
- 如果mutex当中的计数器的值为1,则pthread_mutex_lock接口就返回了,表示加锁成功,同时计数器当中值会被更改为0
- 如果mutex当中的计数器的值为0,则pthread_mutex_lock接口就阻塞了,pthread_mutex_lock接口没有返回,阻塞在该函数的内部,直到加锁成功
函数2:
- int pthread_mutex_trylock(pthread_mutex_t *mutex)
函数说明:
-
该接口是非阻塞加锁接口
-
含义:
- 当互斥锁变量当中的计数器值为1,则加锁成功返回
- 当互斥锁变量当中的计数器值为0,也会返回,但是一定要清楚,加锁是没有成功的,也就是不要去访问临界资源
- 一般非阻塞接口都需要搭配循环来使用
函数3:
- int pthread_mutex_timedlock(pthread_mutex_t *restrict mutex,const struct timespec *restrict abs_timeout)
函数说明:
- 带有超时时间的接口,也就是意味着当不能直接获取互斥锁的时候,会等待abs_timeout时间
- 如果在这个时间内加锁成功了,直接返回,不需要在继续等待剩余的时间,并且表示加锁成功
- 如果超过该时间,也会返回,但是表示加锁失败了,需要循环加锁
3.3.3 解锁 (+1)
函数:
- int pthread_mutex_unlock(pthread_mutex_t *mutex)
函数说明:
- 不管是哪一个加锁接口加锁成功的,都可以使用该接口进行解锁
- 解锁的时候,会将互斥锁变量当中的计数器的值,从0变为1,表示其他线程可以获取互斥锁
3.3.4 销毁互斥锁
函数:
- int pthread_mutex_destroy(pthread_mutex_t *mutex)
函数说明:
- 针对的是动态初始化的互斥锁
3.3.5 代码示例
场景:简单模拟黄牛抢票
- 不加锁
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
//定义票数量
int ticket_val = 20;
//定义线程数量
#define THREADCOUNT 4
//线程入口函数
void* tStart(void* arg)
{
(void)arg;
//一直抢票直到票被抢完
while(1)
{
if(ticket_val > 0)
{
printf("i am thread %p,抢到票:%d\n",pthread_self(),ticket_val);
ticket_val--;
}else{
break;
}
}
return NULL;
}
int main()
{
pthread_t tid[THREADCOUNT];
for(int i=0;i<THREADCOUNT;i++)
{
int ret = pthread_create(&tid[i],NULL,tStart,NULL);
if(ret < 0)
{
perror("pthread_create");
return -1;
}
}
//main线程阻塞等待
for(int i = 0;i<THREADCOUNT;i++)
{
pthread_join(tid[i],NULL);
}
return 0;
}
运行结果:
- 加互斥锁后
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
//定义票数量
int ticket_val = 20;
//定义互斥锁
pthread_mutex_t lock_;
//定义线程数量
#define THREADCOUNT 4
//线程入口函数
void* tStart(void* arg)
{
(void)arg;
//一直抢票直到票被抢完
while(1)
{
//加锁
pthread_mutex_lock(&lock_);
if(ticket_val > 0)
{
printf("i am thread %p,抢到票:%d\n",pthread_self(),ticket_val);
ticket_val--;
//解锁
pthread_mutex_unlock(&lock_);
}else{
//在任何可能导致线程退出的地方都要考虑释放锁
pthread_mutex_unlock(&lock_);
break;
}
}
return NULL;
}
int main()
{
//初始化互斥锁
pthread_mutex_init(&lock_,NULL);
pthread_t tid[THREADCOUNT];
for(int i=0;i<THREADCOUNT;i++)
{
int ret = pthread_create(&tid[i],NULL,tStart,NULL);
if(ret < 0)
{
perror("pthread_create");
return -1;
}
}
//main线程阻塞等待
for(int i = 0;i<THREADCOUNT;i++)
{
pthread_join(tid[i],NULL);
}
//销毁互斥锁
pthread_mutex_destroy(&lock_);
return 0;
}
运行结果:
3.4 线程同步
3.4.1 条件变量概念
- 同步:在保证数据安全的前提下,让线程能够按照某种特定的顺序访问临界资源,即是为了保证各个线程对临界资源访问的合理性
- 条件变量:本质是PCB等待队列+一堆接口(等待接口+唤醒接口)
3.4.2 条件变量的接口
初始化:
函数:
- int pthread_cond_init(pthread_cond_t *restrict cond, const pthread_condattr_t *restrict attr)
函数说明:
- cond:pthread_cond_t,条件变量的类型,传参的时候传入条件变量的地址
- attr:条件变量的属性,通常传递NULL,采用默认属性
等待:将调用该接口的线程放到PCB等待队列中
函数:
- int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex)
函数说明:
- cond:条件变量
- mutex:互斥锁
传互斥锁的作用:
- 同步并没有保证互斥,而保证互斥是使用了互斥锁
- pthread_cond_wait函数内部会进行解锁互斥锁,对于解锁顺序,一定是先放到PCB等待队列中,在进行解锁
函数的内部实现逻辑:
-
将调用pthread_cond_wait函数的执行流放到PCB等待队列中
-
解互斥锁
-
等待被唤醒
-
被唤醒之后:
-
从PCB等待队列中移除出来
-
抢占互斥锁
-
情况1:拿到互斥锁,pthread_cond_wait函数就返回了
-
情况2:没有抢到互斥锁,阻塞在pthread_cond_wait函数内部抢锁的逻辑中
- 当卡在pthread_cond_wait函数内部抢锁的逻辑的执行流一旦时间片耗尽,意味着当前线程被切换出来,程序计数器保存的就是抢锁的命令,上下文信息当中保存的就是寄存器当中的值
- 当再次拥有CPU时间片之后,从程序计数器和上下文信息当中恢复抢锁的逻辑
- 直到抢锁成功,pthread_cond_wait函数才返回
-
-
-
唤醒:
函数:
- int pthread_cond_signal(pthread_cond_t *cond)
- int pthread_cond_broadcast(pthread_cond_t *cond)
作用:
- 通知PCB等待队列当中的线程,将其从队列当中移除出来唤醒该线程
- signal:唤醒至少一个PCB等待队列当中的线程
- broadcast:唤醒PCB等待队列当中全部的线程
释放:
函数:
- int pthread_cond_destroy(pthread_cond_t *cond)
3.4.3 代码示例
场景:生产者线程做一碗面,消费者线程吃一碗面
- 示例1:
定义1个条件变量:
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
//定义线程数量
#define THREADCOUNT 2
int g_bowl = 0;
pthread_mutex_t lock_;
pthread_cond_t g_cond;
//
void* EatStart(void* arg)
{
(void)arg;
while(1)
{
//加锁
pthread_mutex_lock(&lock_);
while(g_bowl<=0)
{
pthread_cond_wait(&g_cond,&lock_);
}
printf("i am thread %p,i eat %d\n",pthread_self(),g_bowl);
g_bowl--;
//通知 做面的人做面
pthread_cond_signal(&g_cond);//随机唤醒一个线程
// pthread_cond_broadcast(&g_cond);//唤醒全部等待的线程
//释放锁
pthread_mutex_unlock(&lock_);
}
return NULL;
}
void* MakeStart(void* arg)
{
(void)arg;
while(1)
{
//加锁
pthread_mutex_lock(&lock_);
while(g_bowl>=1)
{
pthread_cond_wait(&g_cond,&lock_);
}
g_bowl++;
printf("i am thread %p,i make %d\n",pthread_self(),g_bowl);
//释放锁
//通知吃面的人吃面
pthread_cond_signal(&g_cond);//随机唤醒一个线程
// pthread_cond_broadcast(&g_cond); // 唤醒全部的等待的线程
pthread_mutex_unlock(&lock_);
}
return NULL;
}
int main()
{
//初始化互斥锁
pthread_mutex_init(&lock_,NULL);
//初始化条件变量
pthread_cond_init(&g_cond,NULL);
//创建线程
pthread_t eat_tid[THREADCOUNT],make_tid[THREADCOUNT];
for(int i=0;i<THREADCOUNT;i++)
{
int ret = pthread_create(&eat_tid[i],NULL,EatStart,NULL);
if(ret < 0)
{
perror("pthread_create");
return -1;
}
ret = pthread_create(&make_tid[i],NULL,MakeStart,NULL);
if(ret < 0)
{
perror("pthread_create");
return -1;
}
}
//main则塞等待
for(int i = 0;i<THREADCOUNT;i++)
{
pthread_join(eat_tid[i],NULL);
pthread_join(make_tid[i],NULL);
}
//销毁
pthread_mutex_destroy(&lock_);
pthread_cond_destroy(&g_cond);
return 0;
}
运行结果:
-
使用 pthread_cond_signal唤醒,并且生产者与消费者线程只有一个时:
可以看到程序一直运行 -
使用 pthread_cond_signal唤醒,并改变线程数量都各为2时
因为条件变量的本质是PCB等待队列,将生产者与消费者都加入同一个等待队列,所以使用signal唤醒可能还将程序卡死 -
使用pthread_cond_broadcast唤醒全部线程,并改变线程数量都各为2时
可以看到程序在一直运行 -
示例2:
定义两个条件变量:
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
//定义线程数量
#define THREADCOUNT 2
int g_bowl = 0;
//定义锁
pthread_mutex_t lock_;
//定义消费者的条件变量
pthread_cond_t eat_cond;
//定义生产者的条件变量
pthread_cond_t make_cond;
//
void* EatStart(void* arg)
{
(void)arg;
while(1)
{
//加锁
pthread_mutex_lock(&lock_);
while(g_bowl<=0)
{
pthread_cond_wait(&eat_cond,&lock_);
}
printf("i am thread %p,i eat %d\n",pthread_self(),g_bowl);
g_bowl--;
//通知 做面的人做面
pthread_cond_signal(&make_cond);
//释放锁
pthread_mutex_unlock(&lock_);
}
return NULL;
}
void* MakeStart(void* arg)
{
(void)arg;
while(1)
{
//加锁
pthread_mutex_lock(&lock_);
while(g_bowl>=1)
{
pthread_cond_wait(&make_cond,&lock_);
}
g_bowl++;
printf("i am thread %p,i make %d\n",pthread_self(),g_bowl);
//释放锁
//通知吃面的人吃面
pthread_cond_signal(&eat_cond);
pthread_mutex_unlock(&lock_);
}
return NULL;
}
int main()
{
//初始化互斥锁
pthread_mutex_init(&lock_,NULL);
//初始化条件变量
pthread_cond_init(&eat_cond,NULL);
pthread_cond_init(&make_cond,NULL);
//创建线程
pthread_t eat_tid[THREADCOUNT],make_tid[THREADCOUNT];
for(int i=0;i<THREADCOUNT;i++)
{
int ret = pthread_create(&eat_tid[i],NULL,EatStart,NULL);
if(ret < 0)
{
perror("pthread_create");
return -1;
}
ret = pthread_create(&make_tid[i],NULL,MakeStart,NULL);
if(ret < 0)
{
perror("pthread_create");
return -1;
}
}
//main则塞等待
for(int i = 0;i<THREADCOUNT;i++)
{
pthread_join(eat_tid[i],NULL);
pthread_join(make_tid[i],NULL);
}
//销毁
pthread_mutex_destroy(&lock_);
pthread_cond_destroy(&eat_cond);
pthread_cond_destroy(&make_cond);
return 0;
}
运行结果:
可以看到程序一直在运行
4. 死锁
4.1 概念
死锁是指在一组进程中的各个进程均占有不会释放的资源,但因互相申请被其他进程所站用不会释放的资源而处于的一种永久等待状态。
4.2 死锁的四个必要条件
- 互斥条件:一个资源每次只能被一个执行流使用
- 请求与保持条件:一个执行流因请求资源而阻塞时,对已获得的资源保持不放
- 不剥夺条件:一个执行流已获得的资源,在末使用完之前,不能强行剥夺
- 循环等待条件:若干执行流之间形成一种头尾相接的循环等待资源的关系
4.3 避免死锁
- 破坏死锁的四个必要条件
- 加锁顺序一致
- 避免锁未释放的场景,在所有的可能导致执行流退出的地方都进行解锁
- 资源一次性分配
4.4 造成死锁的场景
4.4.1 多个执行流同一个互斥锁
当多个执行流使用同一个互斥锁的时候,有一个执行流获取到了互斥锁之后,但是没有释放互斥锁,导致其他执行流都卡死在加锁的接口当中,这种现象称为死锁
代码示例:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
//定义互斥锁
pthread_mutex_t lock_;
//定义线程数量
#define THREADCOUNT 2
//线程入口函数
void* tStart(void* arg)
{
(void)arg;
//加锁
pthread_mutex_lock(&lock_);
printf("i am thread %p\n",pthread_self());
//未释放锁
return NULL;
}
int main()
{
//初始化互斥锁
pthread_mutex_init(&lock_,NULL);
pthread_t tid[THREADCOUNT];
for(int i=0;i<THREADCOUNT;i++)
{
int ret = pthread_create(&tid[i],NULL,tStart,NULL);
if(ret < 0)
{
perror("pthread_create");
return -1;
}
}
//main线程阻塞等待
for(int i = 0;i<THREADCOUNT;i++)
{
pthread_join(tid[i],NULL);
}
//销毁互斥锁
pthread_mutex_destroy(&lock_);
return 0;
}
运行结果:
4.4.2 多个执行流多个互斥锁
多个执行流,多个互斥锁的情况下,每一个执行流都占有一把互斥锁,但是还要申请对方的互斥锁,这种情况下,就会导致各个执行流都阻塞掉,这种现象称为死锁
代码示例:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
//定义互斥锁
pthread_mutex_t lock_1;
pthread_mutex_t lock_2;
//定义线程数量
#define THREADCOUNT 1
//线程入口函数
void* tStart1(void* arg)
{
(void)arg;
pthread_mutex_lock(&lock_1);
sleep(2);
pthread_mutex_lock(&lock_2);
pthread_mutex_unlock(&lock_2);
pthread_mutex_unlock(&lock_1);
return NULL;
}
void* tStart2(void* arg)
{
(void)arg;
pthread_mutex_lock(&lock_2);
sleep(2);
pthread_mutex_lock(&lock_1);
pthread_mutex_unlock(&lock_1);
pthread_mutex_unlock(&lock_2);
return NULL;
}
int main()
{
//初始化互斥锁
pthread_mutex_init(&lock_1,NULL);
pthread_mutex_init(&lock_2,NULL);
pthread_t tid1[THREADCOUNT],tid2[THREADCOUNT];
for(int i=0;i<THREADCOUNT;i++)
{
int ret = pthread_create(&tid1[i],NULL,tStart1,NULL);
if(ret < 0)
{
perror("pthread_create");
return -1;
}
ret = pthread_create(&tid1[i],NULL,tStart2,NULL);
if(ret < 0)
{
perror("pthread_create");
return -1;
}
}
//main线程阻塞等待
for(int i = 0;i<THREADCOUNT;i++)
{
pthread_join(tid1[i],NULL);
pthread_join(tid2[i],NULL);
}
//销毁互斥锁
pthread_mutex_destroy(&lock_1);
pthread_mutex_destroy(&lock_2);
return 0;
}
运行调试:
- gdb attach [pid]:将进程附加上gdb
- thread apply all bt:查看多个线程堆栈
- t [线程编号]:跳转到具体的线程的堆栈当中,线程编号就是gdb调试的时候,看到的Thread [num]
- f [堆栈编号]:跳转到某一个堆栈当中去