文章目录
- [📖 前言](#📖 前言)
- [1. 线程互斥](#1. 线程互斥)
-
- [1.1 临界资源:](#1.1 临界资源:)
- [1.2 互斥性与原子性:](#1.2 互斥性与原子性:)
-
- [1.2 - 1 概念回顾](#1.2 - 1 概念回顾)
- [1.3 线程安全:](#1.3 线程安全:)
-
- [1.3 - 1 可重入与不可重入](#1.3 - 1 可重入与不可重入)
- [1.4 线程加锁与解锁:](#1.4 线程加锁与解锁:)
-
- [1.4 - 1 竞争锁](#1.4 - 1 竞争锁)
- [1.4 - 2 锁的原子性](#1.4 - 2 锁的原子性)
- [1.5 加锁的原子性如何实现:](#1.5 加锁的原子性如何实现:)
- [1.6 死锁:](#1.6 死锁:)
-
- [1.6 - 1 死锁的演示](#1.6 - 1 死锁的演示)
- [1.6 - 2 一把锁出现死锁的情况](#1.6 - 2 一把锁出现死锁的情况)
- [1.6 - 3 死锁的条件](#1.6 - 3 死锁的条件)
- [2. 线程同步](#2. 线程同步)
-
- [2.1 什么是同步:](#2.1 什么是同步:)
- [2.2 条件变量:](#2.2 条件变量:)
-
- [2.2 - 1 pthread_cond_init / destroy](#2.2 - 1 pthread_cond_init / destroy)
- [2.2 - 2 pthread_cond_wait](#2.2 - 2 pthread_cond_wait)
- [2.2 - 3 唤醒线程](#2.2 - 3 唤醒线程)
- [2.3 代码演示:](#2.3 代码演示:)
-
- [2.3 - 1 同步的现象](#2.3 - 1 同步的现象)
- [2.3 - 2 解决小bug](#2.3 - 2 解决小bug)
- [2.3 - 4 条件变量经典错误](#2.3 - 4 条件变量经典错误)
📖 前言
上一章我们学习了线程的控制,理解了什么是线程id,和线程私有栈等相关的概念,还学习了线程退出的四种方式,并学习了几个相关接口。
本章我们还是继续进行线程方面的学习,接下来我们将学线程的互斥加锁,同步条件变量等待等相关问题的学习,目标已经确定,搬好小板凳准备开讲啦......
1. 线程互斥
1.1 临界资源:
在之前【进程通信 --- 共享内存】
的学习中,我们已经介绍过临界资源,临界区,原子性等概念了,今天我们再来复盘一下:
- 临界资源: 被多个
进程/线程
(执行流)都能够看到并访问的资源叫做临界资源
。
- 如果没有对临界资源进行任何保护,直接对于临界资源的访问。
- 多个
进程/线程
(执行流)在进行访问的时候,就都是乱序的。 - 可能会因为读写交叉而导致的各种乱码、废弃数据、访问控制方面的问题!!
- 临界资源有安全的也有不安全的,取决于内部是否做了保护。
- 临界区: 对多个
进程/线程
(执行流)而言,访问临界资源的代码
。
- 我的
进程/线程
代码中,有大量的代码,只有一部分代码,会访问临界资源。 - 多个
进程/线程
对临界资源做读写的代码,我们称之为临界区。
1.2 互斥性与原子性:
1.2 - 1 概念回顾
- 原子性: 我们把一件事情,要不没做,要么做完了,叫原子性(没有中间状态)。
- 互斥: 任何时刻,只允许一个
进程/线程
,访问临界资源。
在线程中,存在着访问临界资源而导致的冲突:
如果我们要对一个变量进行++/- -要做什么工作呢?
- 假设是对100进行 - - 操作。
-
- 要先将100从内存拷贝到CPU里面。
-
- 然后在CPU里面做好计算。
-
- 最后从CPU中再拷贝内存中。
补充:
CPU内的寄存器是被所有的执行流共享的,但是寄存器里面的数据是属于当前执行流的上下文数据。线程被切换的时候,需要保存上下文,线程被换回的时候,需要恢复上下文。
线程切换时,需要保存的不是寄存器,而是寄存器里面的数据。
所以谈线程必谈两个概念,一个是线程的上下文,另一个是线程的独立站结构。
问题出现:
- 假设第一个进程将数据
100
拷贝到CPU
中进行- -
操作。 - 此时线程替换,切换到第二个线程。
- 第二个线程再将数据从内存总拷贝到
CPU
,执行其他操作(连续- -
到50
)。 - 若第二个线程将数据从
100
减到50
,再切回原来的进程。 - 原来的进程再继续执行,直接把第二个线程好不容易减到
50
的值又干到了99
。
为了保证能够正确的控制线程的访问,其就必须维护自身的原子性!不能有中间状态!!
当我们访问某种资源的时候,任何时刻都只有一个执行流在进行访问,这个就叫做:互斥特性。
为了维护互斥性,我们要给线程的临界资区加锁。
1.3 线程安全:
线程安全是指在多线程环境下,对共享资源的访问不会导致数据不一致或者出现意料之外的结果。
当多个线程同时访问共享资源时,如果没有适当的同步机制或保护措施,可能会导致以下问题:
- 竞态条件(Race Condition):多个线程对同一资源进行读写操作,由于执行顺序不确定,可能导致结果的不确定性、错误的计算结果或数据丢失等问题。
- 数据竞争(Data Race):多个线程同时对同一数据进行读写操作,由于缺乏同步机制,可能导致数据的不一致性或错误的结果。
为了确保线程安全,需要采取合适的并发控制措施,如加锁机制、原子操作、信号量等。这些机制可以保证在任意时刻只有一个线程能够访问共享资源,避免数据竞争和竞态条件的发生。
1.3 - 1 可重入与不可重入
- 同一个函数被不同的执行流调用,当前一个流程还没有执行完,就有其他的执行流再次进入,我们称之为重入。
- 一个函数在重入的情况下,运行结果不会出现任何不同或者任何问题,则该函数被称为可重入函数,否则,是不可重入函数。
面对不可重入函数,这不是问题,这是种特性,一般通过加锁来解决。
加锁本质上是把多线程串行起来,确保同一时间只有一个线程可以进入临界区,让每个线程可以安全的调用这个不可重入的函数。
如果一个库函数明明告知你了是不可重入的,但是还不加保护的在多线程操作中调用它。
那么这段代码如果出现bug,并不是库函数本身有问题,是编码的问题。
注意:
- 重入是种现象,不是问题(特性)。
- 一个函数被重复进入,叫做被重入。
- 在被重复进入的情况下,执行代码出了问题,叫做该函数是不可重入函数。
- 如果没有出现问题,叫做可重入。
一个线程对全局变量的恶意修改 ,可能会影响其他线程安全。
一个线程有bug导致线程退出,导致其他线程也退出,也叫做线程退出。
绝大多数的系统自带的库(比如C++的STL库)都是不可重入的,并非所有容器都是线程安全的。
1.4 线程加锁与解锁:
- 加锁:
定义全局的互斥锁,所有线程能访问:
初始化互斥量有两种方法:
方法一:静态分配
cpp
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER
方法二:动态分配
cpp
int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);
// 参数:
// mutex:要初始化的互斥量
// attr:NULL
- 解锁 / 销毁锁:
销毁互斥量需要注意:
- 使用
PTHREAD_MUTEX_INITIALIZER
初始化的互斥量不需要销毁。 - 不要销毁一个已经加锁的互斥量。
- 已经销毁的互斥量,要确保后面不会有线程再尝试加锁。
1.4 - 1 竞争锁
多线程竞争锁:
- 如果多个线程同时对资源进行读写操作,可能会引发竞争条件(Race Condition),导致数据不一致或其他问题。
- 为了避免竞争条件,线程必须在访问共享资源之前先获取互斥锁。
- 当一个线程获得互斥锁后,其他线程就无法再获取该锁,只能等待锁被释放。
- 这样可以确保每次只有一个线程访问共享资源,保证数据的一致性和正确性。
因此,线程互斥锁的使用涉及到多个线程之间的竞争获取锁的过程,以确保同一时间只有一个线程能够获得互斥锁,并访问共享资源。
其他线程已经锁定互斥量(互斥锁),或者存在其他线程同时申请互斥量,但没有竞争到互斥量,那么
pthread_ lock
调用会陷入阻塞(执行流被挂起),等待互斥量解锁。
我们用多线程实现一个简单的抢票程序:
cpp
#include <iostream>
#include <pthread.h>
#include <cstdio>
#include <cstring>
#include <unistd.h>
using namespace std;
// int 票数计数器
// 临界资源
int tickets = 10000; // 临界资源,可能会因为共同访问,可能会造成数据不一致问题。
// 定义一个全局的锁
pthread_mutex_t mutex;
void* getTickets(void* args)
{
const char* name = static_cast<const char*>(args);
while (true)
{
// 加锁
pthread_mutex_lock(&mutex);
if (tickets > 0)
{
usleep(1000);
cout << name << " 抢到了票, 票的编号: " << tickets << endl;
tickets--;
// 解锁
pthread_mutex_unlock(&mutex);
//other code
usleep(123); //模拟其他业务逻辑的执行
}
else
{
// 票抢到几张,就算没有了呢?0
cout << name << "] 已经放弃抢票了,因为没有了..." << endl;
// 解锁
pthread_mutex_unlock(&mutex);
break;
}
}
return nullptr;
}
int main()
{
// 对锁初始化
pthread_mutex_init(&mutex, nullptr);
pthread_t tid1;
pthread_t tid2;
pthread_t tid3;
pthread_create(&tid1, nullptr, getTickets, (void*)"thread1");
pthread_create(&tid2, nullptr, getTickets, (void*)"thread2");
pthread_create(&tid3, nullptr, getTickets, (void*)"thread3");
int n = pthread_join(tid1, nullptr);
cout << n << ":" << strerror(n) << endl;
pthread_join(tid2, nullptr);
cout << n << ":" << strerror(n) << endl;
pthread_join(tid3, nullptr);
cout << n << ":" << strerror(n) << endl;
// 锁用完了,释放这把锁
pthread_mutex_destroy(&mutex);
return 0;
}
如果不加锁的话,if
条件判断是否也会有线程安全问题:
if
判断的时候也有可能出现问题,因为CPU也需要参与。-
- 假设票数是1,线程A还没开始减减的时候,就被切走了。
-
- 线程B来了,也要从内存读到CPU里,判断票数为1,线程B也要减减。
-
- 两个线程都要减减,就会出现将票减到负数的情况。
- 注意:当执行完if条件判断后,CPU会将tickets变量的值写回内存,然后再执行tickets - - 操作时,会从内存中读取最新的tickets值进行减减操作。
所以就导致了多线程进行抢票的时候出现了负数票的情况:
加锁的范围:
- 临界区,只要对临界区加锁,而且加锁的粒度越细越好。
- 加锁的本质是让线程执行临界区代码串行化,不能对所有代码加锁,只能对临界区加锁(加锁要短小惊叹)。
- 加锁是一套规范,通过临界区对临界资源进行访问的时候,要加就都要加。
- 锁保护的是临界区,任何线程执行临界区代码访问临界资源,都必须先申请锁,前提是都必须先看到锁!
1.4 - 2 锁的原子性
这把锁,本身不就也是临界资源吗,谁来给它加锁呢?锁的设计者早就想到了~
pthread_mutex_lock
竞争和申请锁的过程,就是原子的!
难度在加锁的临界区里面,就没有线程切换了吗??
- 我在临界资源对应的临界区中就锁了,还是是多行代码,还可以被切换!
- 线程被切走,是完全可以,因为线程执行的加锁解锁等对应的也是代码。
- 线程在任意代码出都可以被切换!!
- 但是线程加锁是原子的,最后的结果无非就是:要么拿到了锁,要么没有拿到。
- 当多个线程竞争锁的时候,不存在中间状态,不存在锁被拿了一半。
当加锁的线程被切走的时候,绝不会有其他线程进入临界区!!
- 每个线程进入临界区都必须先申请锁!!
- 假如当前的锁,被线程A申请走了。
- 即便当前的线程A没有被调度(它是从CPU切走了),但是它是抱着锁走的!!
- 一旦一个线程持有了锁,该线程根本就不担心任何的切换问题!
- 对于其他线程而言,线程A访问临界区,只有没有进入和使用完毕两种状态,才对其他线程有意义!
所有线程要想进入临界区,就得申请锁,只要有了锁才能进入临界区。
当一个线程有了锁,就不害怕被切换了。
进入临界区前,都必须要申请锁,当一个线程申请锁之后,另一个线程只能等,要么使用临界区使用完了,要么根本就没有进入临界区。
在执行临界区代码时,加锁的那部分代码,会不会切换呢?会的!或者阻塞呢?会的!或者挂起呢?会的!
1.5 加锁的原子性如何实现:
为了实现互斥锁操作,大多数体系结构都提供了swap
或exchange
指令,该指令的作用是把寄存器和内存单元的数据相交换,由于只有一条指令,保证了原子性。
现在我们看一下pthread_mutex_lock
和pthread_mutex_unlock
的伪代码:
- %al:寄存器在CPU内。
- mutex:内存中的一个变量,默认值是1。
- 寄存器的值,和内存的值,用一条语句做交换。
- 寄存器的内容如果大于0:
-
- 则代表申请锁成功。
- 寄存器的内容如果小于0:
-
- 就等待挂起。
- 凡是在寄存器中的数据,全部都是线程的内部上下文!!
- 多个线程看起来同时在访问寄存器,但是互不影响。
寄存器只有一个,是被所有线程共享的,但是寄存器里的内容是被所有线程私有的。单CPU任何时刻,只能有一个线程在跑。
每个线程要切走时,必须将自己的上下文带走。
过程:
- 线程A申请锁:
-
- 线程A要申请锁,先执行将0写入到寄存器当中。
-
- 然后将
%a寄存器
中的0和内存中mutex的值交换。
- 然后将
-
- 此时
%a寄存器
中的值是1,然后线程A再被切走。
- 此时
-
- 切走后,寄存器内的值会被保存在线程的上下文中一并被带走。
- 线程B申请锁:
-
- 线程B来的时候,线程A必须从CPU上剥离下去,剥离走就必须将上下文带走。
-
- 线程B再来执行加锁代码。
-
- 线程B再将0写到寄存器当中。
-
- 然后将
%a寄存器
中的0和内存中mutex的值交换。
- 然后将
-
- 此时内存中mutex的值是0,这是时候就相当于0和0的交换。
-
%a寄存器
的内容不大于0,线程B挂起等待。
-
- 此时线程B就叫申请锁失败了。
用一条汇编的方式,把寄存器的值放到mutex,把mutex的值放到了寄存器,这个动作就叫做加锁。
本质:
- 将数据从内存读入寄存器,本质是将数据从共享变成线程私有!
- 将自己的锁拿到线程自己的上下文里。
- 只要交换到了,以后还有线程想申请锁就不可能拿到了。
解锁:解锁,就是把1再写回去mutex,这就完成了解锁。
1.6 死锁:
1.6 - 1 死锁的演示
模拟死锁,出现的情况:
- 两个线程拿着对方要的锁,自己又抱着一把锁。
- 线程1拿着A,线程2拿着B,但是这两个线程又都向对方要锁。
- 互相申请对方的锁,但是自己要的锁已经被对方拿走了。
现象:
- 线程1申请B锁的时候,锁被线程2拿着了,申请不到,这个线程1就被挂起了(抱着A挂起的)。
- 线程2申请A锁的时候,锁被线程1拿着了,申请不到,这个线程2就被挂起了(抱着B挂起的)。
- 这就导致了死锁的问题。
cpp
#include <iostream>
#include <cstdio>
#include <cstring>
#include <unistd.h>
#include <pthread.h>
#include "Lock.hpp"
using namespace std;
// 模拟死锁
// 静态定义锁的方式(可以不用再去destroy,也可以不用对其进行init)
pthread_mutex_t mutexA = PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_t mutexB = PTHREAD_MUTEX_INITIALIZER;
void *startRoutine1(void *args)
{
while (true)
{
pthread_mutex_lock(&mutexA);
sleep(1);
pthread_mutex_lock(&mutexB);
cout << "我是线程1,我的tid: " << pthread_self() << endl;
pthread_mutex_unlock(&mutexA);
pthread_mutex_unlock(&mutexB);
}
}
void *startRoutine2(void *args)
{
while (true)
{
pthread_mutex_lock(&mutexB);
sleep(1);
pthread_mutex_lock(&mutexA);
cout << "我是线程2,我的tid: " << pthread_self() << endl;
pthread_mutex_unlock(&mutexB);
pthread_mutex_unlock(&mutexA);
}
}
int main()
{
pthread_t t1, t2;
pthread_create(&t1, nullptr, startRoutine1, nullptr);
pthread_create(&t2, nullptr, startRoutine2, nullptr);
pthread_join(t1, nullptr);
pthread_join(t2, nullptr);
return 0;
}
没有结果一直在阻塞中:
循环查看一下线程:
1.6 - 2 一把锁出现死锁的情况
一把锁会不会有死锁问题呢?我们来演示一下:
cpp
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
int cnt = 100;
using namespace std;
void *startRoutine(void *args)
{
string name = static_cast<char *>(args);
while (true)
{
pthread_mutex_lock(&mutex);
cout << name << " count : " << cnt-- << endl;
// 代码写错了,写了两个锁
// pthread_mutex_lock(&mutex);
pthread_mutex_unlock(&mutex);
// 耗时的操作尽量不要再临界区里面
sleep(1);
}
}
编码错误引起的:
那就是同时申请了一把锁两次,这就导致第二次申请锁的时候一直在等待第一把锁释放,这就导致了死锁问题。
1.6 - 3 死锁的条件
死锁的条件通常被称为"死锁四个必要条件",它们是:
- 互斥条件:至少有一个资源同时只能被一个线程(或进程)占用,不能同时被多个线程访问。
- 请求与保持条件:一个线程在持有一个资源的同时,又请求获取其他的资源。
- 不可剥夺条件:已分配的资源无法被强制性地抢占,只能由占有它的线程主动释放。
- 循环等待条件:存在一组线程,每个线程都在等待下一个线程所持有的资源,形成一个循环等待链。
当以上四种条件同时满足时,就可能发生死锁。要解决死锁问题,需要破坏其中至少一个条件。例如,可以采用资源有序分配、避免持有并等待、引入抢占机制以及打破循环等待等方法来预防和解除死锁。
2. 线程同步
2.1 什么是同步:
线程互斥,它是对的,难道它任何场景都是合理的吗?很显然并不是。
互斥有可能导致饥饿问题:
- 一个执行流,长时间得不到某种资源。
- 例如一个线程的优先级很高,每次竞争锁时,都可以优先竞争到。
- 这就导致了其他线程竞争不到锁,造成了饥饿问题。
在保证临界资源安全的前提下(互斥等),让线程访问某种资源,具有一定的顺序性,称之为同步!
- 防止饥饿线程协同。
- 同步和互斥不是对立的关系,而是互相补充的关系。
- 互斥保证了数据的安全,同步不一定需要互斥。
一般在保证互斥前提条件下,多做了一个工作,让多个线程访问某种资源具备了一定的顺序性,这种特性我们称之为,同步。
2.2 条件变量:
如何完成同步呢?
Linux中提供了完成同步的重要机制,叫做:条件变量。(最常用,没有之一,最常用的线程同步的策略)
条件变量:
- 当一个线程互斥地访问某个变量时,它可能发现在其它线程改变状态之前,它什么也做不了。
- 例如一个线程访问队列时,发现队列为空,它只能等待,只到其它线程将一个节点添加到队列中。
- 这种情况就需要用到条件变量。
条件变量要和mutex互斥锁,一并使用!
2.2 - 1 pthread_cond_init / destroy
基本的接口和pthread
库的其他接口很相似,用法也几乎一样。其中attr
也是设置条件变量的属性,这里置为nullptr
即可。
2.2 - 2 pthread_cond_wait
- 如上两个接口都是让线程在一个条件变量下进行等待。
- 其中
timewait
接口可以在条件变量下等,设置等待的时间(超时了就不等了)。 - 条件变量也是临界资源,所以这里需要一个
mutex
互斥锁来保证条件变量读写的原子性。
2.2 - 3 唤醒线程
broadcast
是给在当前条件变量等待的所有线程发信号唤醒,而signal
是发送信号只唤醒一个线程。
条件变量决定,什么时候叫醒一个线程,以前只要有锁,如果所有线程都被叫醒,大家都去参与竞争,谁抢到了算谁的,这个机制完全是由调度器决定的。
2.3 代码演示:
下面的代码可以很好的演示上述的函数接口:
cpp
#include <iostream>
#include <vector>
#include <cstring>
#include <functional>
#include <unistd.h>
#include <pthread.h>
using namespace std;
// 定义一个条件变量
pthread_cond_t cond;
// 定义一个互斥锁
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; // 当前不用,但是接口需要,所以我们需要留下来
// 定义全局退出变量
volatile bool quit = false;
// 三个线程都会调用这个函数
void *waitCommand(void *args)
{
string name = static_cast<char*>(args);
pthread_mutex_lock(&mutex);
// 如果不退出一直去运行
while (!quit)
{
// pthread_cond_wait内部先解锁
pthread_cond_wait(&cond, &mutex);
// 被唤醒出来之后锁已经加上了
cout << "thread id: " << pthread_self() << " running... " << name << endl;
}
pthread_mutex_unlock(&mutex);
cout << "thread quit..." << (char*)args << endl;
return nullptr;
}
int main()
{
// 初始化一个条件变量
pthread_cond_init(&cond, nullptr);
// 创建三个线程
pthread_t t1, t2, t3;
pthread_create(&t1, nullptr, waitCommand, (void*)"thread1");
pthread_create(&t2, nullptr, waitCommand, (void*)"thread2");
pthread_create(&t3, nullptr, waitCommand, (void*)"thread3");
// 主线程控制
while(true)
{
char n;
// cin和cout在交叉使用的时候,缓冲区会做强制刷新
cout << "请输入你的command: ";
cin >> n;
if(n == 'n')
{
// 唤醒在特定条件变量下等的线程
pthread_cond_signal(&cond);
}
else if (n == 'x')
{
pthread_cond_broadcast(&cond);
}
else
{
quit = true;
break;
}
usleep(100);
}
// 唤醒所有等待的条件变量
pthread_cond_broadcast(&cond);
cout << "main thread quit..." << endl;
// 释放条件变量
pthread_cond_destroy(&cond);
pthread_mutex_destroy(&mutex);
// 等待线程
int m = pthread_join(t1, nullptr);
cout << strerror(m) << endl;
m = pthread_join(t2, nullptr);
cout << strerror(m) << endl;
m = pthread_join(t3, nullptr);
cout << strerror(m) << endl;
return 0;
}
运行结果:
2.3 - 1 同步的现象
现象:
- 我们看到,如果一个一个唤醒时,所有线程是以队列的形式在排队的,当在排队的时候,明显能看到,执行时有一定的顺序性的。
- 当我们将全部线程都唤醒时,发现所有等待的线程全部都被唤醒并运行起来。
但是我们退出时,将quit改成true,循环条件不满足,三个线程退出,将三个线程全部唤醒,我们却发现,阻塞等待住了,这是什么原因:(重点)
- 判断一个条件是否满足,一定是在临界区内部才能得出条件是否满足,进而才能决定是否要挂起等待还是继续运行。
- 所以一定是占有锁的情况。
- 一旦占有锁了,把锁占了但是又去条件变量下去等了,那么谁来释放这个锁呢?
- 该线程拿着锁去休眠了,导致其他线程无法申请该锁,这就有问题了。
pthread_cond_wait
内部已经帮我们想到了这一点,并做了相应的措施:(重点)
cpp
int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex)
{
pthread_mutex_unlock(mutex);// 先解锁
// 避免因为该线程拿着锁去休眠了,导致其他线程无法申请该锁
//条件变量相关代码
pthread_mutex_lock(mutex);// 条件满足后,再加锁
}
pthread_cond_wait
内部是先调用pthread_mutex_unlock
解锁,再调用pthread_mutex_lock
加锁。
知道了pthread_cond_wait
内部细节,再来回头看阻塞的原因:(重点)
- 因为有多个线程,当被同时唤醒的时候,
pthread_cond_wait
内部都要重新加锁,就要同时去竞争一把锁,但是只有一个能竞争成功。 - 最后其他线程竞争不到锁,就在锁那里被阻塞了,所以看到了只有一个线程退出成功了。
- 那么其他线程就没有办法去退出了(一直卡在
pthread_cond_wait
函数最后加锁那里,等着申请锁),而且那个线程退出了也没有解锁,就导致其他线程无法占用这个锁,就一直在阻塞等待锁。 - 最后就导致其他线程无法退出。
2.3 - 2 解决小bug
解决问题:在退出循环后解锁(重点)
- 在退出循环后解锁,就可以很好的解决问题,使得每个线程都能退出。
-
- 当我们输入
q
之后,false被改成true,但是此时所有线程还是在条件变量中等。
- 当我们输入
-
- 当主线程将所有的在条件变量中等待的新线程都唤醒了,
pthread_cond_broadcast
之后。
- 当主线程将所有的在条件变量中等待的新线程都唤醒了,
-
- 所有的线程都醒了过来,都要去加锁,只有一个线程抢到了锁。
-
- 此时继续往下执行,循环条件不成立,退出循环然后解锁。
-
- 解完锁之后,剩下的线程还处于抢锁的状态。
-
- 然后剩下的线程中,有一个拿到了锁,继续上述过程,退出循环然后解锁。
-
- 然后剩下的线程中,有一个拿到了锁,......
-
- 直到所有线程都退出。
为什么主线程输入n
多个线程会挨个执行?
- 因为多个线程同时跑一起来之后,同时去申请了锁。
- 只有一个能抢得到锁,其余线程都在申请锁那里阻塞住了。
- 然后抢到锁的线程,条件等待,
pthread_cond_wait
内部解锁了。 - 然后剩下的线程再去抢锁,其余线程都在申请锁那里阻塞住了。
- 然后抢到锁的线程,条件等待,
pthread_cond_wait
内部解锁了。 - ......
- 直到最后所有的线程都已经在条件变量中等了。
- 主线程输入
n
,pthread_cond_signal
唤醒一个线程,然后加锁。 - 该线程再次拿到了锁(因为其他线程都在条件变量里等了,没有线程和这个线程强锁),执行剩余代码(打印)。
- 该线程while循环判断,再次进入循环,再次进入条件变量等待,
pthread_cond_wait
内部解锁了,线程继续等待。 - 主线程中的
pthread_cond_signal
执行完毕,主线程的while循环再次执行。 - 再次输入
n
,pthread_cond_signal
唤醒另一个线程,具体唤醒哪一个,我也不知道。 - 当多个线程同时等待同一个条件变量时,它们可能会被挨个唤醒。具体哪个线程先被唤醒取决于操作系统的调度机制。
- 唤醒之后,该拿到了锁,继续上述过程......
2.3 - 4 条件变量经典错误
cpp
pthread_mutex_lock(&mutex);
while (condition_is_false) {
pthread_mutex_unlock(&mutex);
pthread_cond_wait(&cond);// 解锁和加锁的操作,该函数会帮我们完成
pthread_mutex_lock(&mutex);// 二次申请同一把锁,出现死锁!
}
pthread_mutex_unlock(&mutex);
- 由于解锁和等待不是原子操作,调用解锁之后,pthread_cond_wait 之前。
- 如果已经有其他线程获取到互斥量,摒弃条件满足,发送了信号,那么pthread_cond_wait 将错过这个信号。
- 可能会导致线程永远阻塞在这个pthread_cond_wait 。
- 所以解锁和等待必须是一个原子操作。
当一个线程解锁互斥量之后,另一个线程可能立刻获取到互斥量,并满足条件发送信号,然而此时还未执行pthread_cond_wait函数,因此等待的线程可能会错过这个信号,从而导致永远等待下去,这就是典型的竞态条件问题。
了解一下吧,我也不太明白......