目录
一.Linux线程互斥
1.进程线程间的互斥相关背景概念
2.互斥量mutex引出
3.互斥量接口
4.互斥量原理探究
二.可重入VS线程安全
1.基本概念
2.常见的线程不安全的情况
3.常见的线程安全的情况
4.常见的不可重入的情况
5.常见的可重入的情况
6.可重入与线程安全联系
7.可重入与线程安全区别
三.常见锁概念
1.死锁
2.死锁的四个必要条件
3.避免死锁
四.Linux线程同步
1.同步概念与竞态条件
2.条件变量
3.条件变量函数
4.为什么pthread_cond_wait需要互斥量
(1)临界资源和临界区
①代码,主线程和新线程都对count++操作
#include
#include
#include
int count = 0; //全局
void* Routine(void* arg)
{
while (1){
count++;
sleep(1);
}
pthread_exit((void*)0);
}
int main()
{
pthread_t tid;
pthread_create(&tid, NULL, Routine, NULL);
while (1){
printf("count: %d\n", count);
sleep(1);
}
pthread_join(tid, NULL);
return 0;
}
②结果: 全局变量count就叫做临界资源,因为它被多个执行流共享,线程执行函数中printf和count++就叫做临界区,因为这些代码对临界资源进行了访问。
(2)互斥和原子性
多线程情况下,如果多个执行流都对同一份临界资源进行访问可能导致数据不一致的问题。解决该问题的方案就叫做互斥,互斥的作用就是,保证在任何时候有且只有一个执行流进入临界区对临界资源进行访问。
①代码: 模拟抢票系统
#include
#include
#include
int tickets = 1000;
void* routine(void* arg) //抢票
{
const char* name = (char*)arg;
while (1){
if (tickets > 0){
usleep(1000);
printf("[%s] get a ticket, left: %d\n", name, --tickets);
}
else{
break;
}
}
printf("%s quit!\n", name);
pthread_exit((void*)0);
}
int main()
{
pthread_t t1, t2, t3, t4;
pthread_create(&t1, NULL, routine, (void*)"thread 1");
pthread_create(&t2, NULL, routine, (void*)"thread 2");
pthread_create(&t3, NULL, routine, (void*)"thread 3");
pthread_create(&t4, NULL, routine, (void*)"thread 4");
pthread_join(t1, NULL);
pthread_join(t2, NULL);
pthread_join(t3, NULL);
pthread_join(t4, NULL);
return 0;
}
②结果:没有对临界资源进行保护,出现了负数
③出现负数原因分析
--tickets实际的操作过程
1.对一个变量进行++,--操作需要三个步骤
2.汇编代码
3.--tickets的过程中有三条汇编指令,其中在执行任何一条指令时都有可能被切走
解决上述抢票系统的问题,需要做到三点:
(1) 初始化
函数:
int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);
参数说明:
- mutex:需要初始化的互斥量。
- attr:初始化互斥量的属性,一般设置为NULL即可。
返回值:
- 互斥量初始化成功返回0,失败返回错误码。
调用pthread_mutex_init函数初始化互斥量叫做动态分配,除此之外,我们还可以用下面这种方式初始化互斥量,该方式叫做静态分配:
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
(2)销毁
函数 : int pthread_mutex_destroy(pthread_mutex_t *mutex);
参数:mutex:需要销毁的互斥量。返回值: 互斥量销毁成功返回0,失败返回错误码。
销毁互斥量需要注意:
- 使用PTHREAD_MUTEX_INITIALIZER初始化的互斥量不需要销毁。
- 不要销毁一个已经加锁的互斥量。
- 已经销毁的互斥量,要确保后面不会有线程再尝试加锁
(3)加锁
函数:int pthread_mutex_lock(pthread_mutex_t *mutex);
参数:mutex:需要加锁的互斥量。
返回值:互斥量加锁成功返回0,失败返回错误码。调用pthread_mutex_lock时,可能会遇到以下情况:
- 互斥量处于未锁状态,该函数会将互斥量锁定,同时返回成功。
- 发起函数调用时,其他线程已经锁定互斥量,或者存在其他线程同时申请互斥量,但没有竞争到互斥量,那么pthread_mutex_lock调用会陷入阻塞(执行流被挂起),等待互斥量解锁。
(4)解锁
函数: int pthread_mutex_unlock(pthread_mutex_t *mutex);
参数: mutex:需要解锁的互斥量。返回值:互斥量解锁成功返回0,失败返回错误码。
(5)使用示例
①代码 : 上述的抢票系统中引入互斥量,每一个线程要进入临界区之前都必须先申请锁,只有申请到锁的线程才可以进入临界区对临界资源进行访问,并且当线程出临界区的时候需要释放锁,这样才能让其余要进入临界区的线程继续竞争锁。
#include
#include
#include
pthread_mutex_t lock;
int tickets = 1000;
void* routine(void* arg) //
{
const char* name = (char*)arg;
while (1){
pthread_mutex_lock(&lock); //加锁
if (tickets > 0){
usleep(100);
printf("[%s] get a ticket, left: %d\n", name, --tickets);
pthread_mutex_unlock(&lock); //解锁
}
else{
pthread_mutex_unlock(&lock);//解锁
break;
}
}
printf("%s quit!\n", name);
pthread_exit((void*)0);
}
int main()
{
pthread_mutex_init(&lock,NULL);
pthread_t t1, t2, t3, t4;
pthread_create(&t1, NULL, routine, (void*)"thread 1");
pthread_create(&t2, NULL, routine, (void*)"thread 2");
pthread_create(&t3, NULL, routine, (void*)"thread 3");
pthread_create(&t4, NULL, routine, (void*)"thread 4");
pthread_join(t1, NULL);
pthread_join(t2, NULL);
pthread_join(t3, NULL);
pthread_join(t4, NULL);
pthread_mutex_destroy(&lock);
return 0;
}
②结果 : 不再出现票为负数的情况
(6)提示
①对临界区进行保护,所有的执行线程都必须遵守这个规则(编码规则,加锁) ;先加锁->访问临界区->再解锁
②所有的线程必须看到同一把锁,锁本身就是临界资源!锁本身先保证自身安全! 申请锁的过程,不能有中间状态,也就是两态。加锁和解锁必须是原子的。
③lock ->访问临界区(花时间)->unlock,在特定进程/线程拥有锁的时候,期间有新线程过来申请锁,一定申请不到 ! 新线程如何? 阻塞,将进程/线程对应的PCB投入到等待队列,unlock后,进行进程/线程唤醒操作!
(1)临界区内的线程可能进行线程切换
(2)锁是否需要被保护?
(3)加锁解锁原子性探究
OS运行的简单理解
①通过lock和unlock的伪代码理解
②可以把mutex想象成一个变量,初始值为1在内存中,%al是计算机中的一个寄存器,当线程申请锁时,需要执行以下步骤:
③理解上下文保护 : 上下文信息保存在内存里 , 上下文切换时将寄存器的值保存在这样的结构里
④ 现在假设有线程A和线程B两个线程,线程A先运行
1)如果线程A执行完步骤1时被切换走,线程B开始执行步骤1
线程A执行步骤1时还未交换mutex的值,线程A有上下文保护,不受影响
2)如果线程A执行完步骤2时被切换走,线程B开始执行步骤2
线程A执行完步骤2,线程A保存自己的寄存器值1,上下文保护将寄存器值1带走了,此时的内存值为0. 线程B拿自己的上下文数据也要和内存的值进行交换,没意义, 交换完还是0和0,线程B继续向下执行就被挂起了;再次切换线程时线程B要进程上下文保存,%al是0,线程A上下文恢复%al的值是1,就return 0,进来执行操作访问临界资源
⑤当线程释放锁时,需要执行以下步骤:
⑥补充
单执行流可能产生死锁吗?
单执行流也有可能产生死锁,如果某一执行流连续申请了两次锁,那么此时该执行流就会被挂起。因为该执行流第一次申请锁的时候是申请成功的,但第二次申请锁时因为该锁已经被申请过了,于是申请失败导致被挂起直到该锁被释放时才会被唤醒,但是这个锁本来就在自己手上,自己现在处于被挂起的状态根本没有机会释放锁,所以该执行流将永远不会被唤醒,此时该执行流也就处于一种死锁的状态。
(1)测试示例
①代码
#include
#include
pthread_mutex_t mutex;
void* Routine(void* arg)
{
pthread_mutex_lock(&mutex);
pthread_mutex_lock(&mutex);
pthread_exit((void*)0);
}
int main()
{
pthread_t tid;
pthread_mutex_init(&mutex, NULL);
pthread_create(&tid, NULL, Routine, NULL);
pthread_join(tid, NULL);
pthread_mutex_destroy(&mutex);
return 0;
}
②结果 : 查看该进程时可以看到,该进程当前的状态是sl+,其中中 l 实际上就是lock的意思,表示该进程当前处于一种死锁的状态。
(2) 阻塞(挂起等待)的理解
①进程运行时是被CPU调度的,进程在调度时是需要用到CPU资源,每个CPU都有一个运行等待队列(runqueue),CPU在运行时就是从该队列中获取进程进行调度的。
②运行等待队列中的进程本质上就是在等待CPU资源,实际上不止是等待CPU资源如此,等待其他资源也是如此,比如锁的资源、磁盘的资源、网卡的资源等等,它们都有各自对应的资源等待队列。
③详细的过程
④精简过程
⑤小结
注意: 只有同时满足了这四个条件才可能产生死锁。
单独使用互斥容易导致饥饿问题,为了解决引入了同步:
条件变量是利用线程间共享的全局变量进行同步的一种机制,条件变量是用来描述某种资源是否就绪的一种数据化描述。
条件变量主要包括两个动作:
条件变量通常需要配合互斥锁一起使用。
(1)初始化
函数:
int pthread_cond_init(pthread_cond_t *restrict cond, const pthread_condattr_t *restrict attr);
参数:
- cond:需要初始化的条件变量。
- attr:初始化条件变量的属性,一般设置为NULL即可。
返回值:条件变量初始化成功返回0,失败返回错误码。
调用pthread_cond_init函数初始化条件变量叫做动态分配,除此之外,我们还可以用下面这种方式初始化条件变量,该方式叫做静态分配:pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
(2)销毁
函数: int pthread_cond_destroy(pthread_cond_t *cond);
参数: cond:需要销毁的条件变量。
返回值: 条件变量销毁成功返回0,失败返回错误码。
销毁条件变量需要注意:使用PTHREAD_COND_INITIALIZER初始化的条件变量不需要销毁。
(3)等待条件变量
函数:
int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex);
参数:
- cond:需要等待的条件变量。
- mutex:当前线程所处临界区对应的互斥锁。
返回值: 函数调用成功返回0,失败返回错误码。
(4)唤醒
函数:
int pthread_cond_broadcast(pthread_cond_t *cond); int pthread_cond_signal(pthread_cond_t *cond);
区别:
- pthread_cond_signal函数用于唤醒等待队列中首个线程。
- pthread_cond_broadcast函数用于唤醒等待队列中的全部线程。
参数: cond:唤醒在cond条件变量下等待的线程。
返回值: 函数调用成功返回0,失败返回错误码。
(5)使用示例
①代码: 主线程控制新线程活动。新线程创建后都在条件变量下进行等待,直到主线程检测到键盘有输入时才唤醒一个等待线程
#include
#include
#include
pthread_mutex_t lock;
pthread_cond_t cond;
void* Run(void* arg)
{
pthread_detach(pthread_self());
std::cout<< (char*)arg << "run ..." << std::endl;
while(true){
pthread_cond_wait(&cond,&lock);//阻塞在这里
std::cout << (char*)arg << " : " << pthread_self() << " active ..." << std::endl;
}
}
int main()
{
pthread_mutex_init(&lock,nullptr);
pthread_cond_init(&cond ,nullptr);
pthread_t t1,t2,t3;
pthread_create(&t1 ,nullptr , Run , (void*)"thread 1");
pthread_create(&t2 ,nullptr , Run , (void*)"thread 2");
pthread_create(&t3 ,nullptr , Run , (void*)"thread 3");
//主线程控制新线程的任务ctrl
while(true){
getchar();
pthread_cond_signal(&cond); //唤醒在该条件变量下等的一个线程
// pthread_cond_broadcast(&cond);//唤醒所有线程,
}
pthread_mutex_destroy(&lock);
pthread_cond_destroy(&cond);
return 0;
}
②结果: 有序唤醒等待的线程,某个线程唤醒后执行完相关操作在该环境变量的等待队列的队尾继续等待 .
小结:
(1)错误的程序设计
当我们进入临界区上锁后,如果发现条件不满足,那我们先解锁,然后在该条件变量下进行等待也可以吧:
pthread_mutex_lock(&mutex);
while (condition_is_false){
pthread_mutex_unlock(&mutex);
//解锁之后,等待之前,条件可能已经满足,信号已经发出,但是该信号可能被错过
pthread_cond_wait(&cond);
pthread_mutex_lock(&mutex);
}
pthread_mutex_unlock(&mutex);
(2)条件变量使用规范
①条件变量等待
pthread_mutex_lock(&mutex);
while (条件为假)
pthread_cond_wait(&cond, &mutex);
修改条件
pthread_mutex_unlock(&mutex);
②唤醒等待
pthread_mutex_lock(&mutex);
设置条件为真
pthread_cond_signal(&cond);
pthread_mutex_unlock(&mutex);