【Linux多线程】线程的同步与互斥
目录
- 【Linux多线程】线程的同步与互斥
- 分离线程
- Linux线程互斥
- 进程线程间的互斥相关背景概念
- 问题产生的原因:
- 互斥量mutex
- 互斥量的接口
- 互斥量实现原理探究
- 对锁进行封装(C++11lockguard锁)
- 可重入VS线程安全
- 概念
- 常见的线程不安全的情况
- 常见的线程安全的情况
- 常见不可重入的情况
- 常见可重入的情况
- 可重入与线程安全联系
- 可重入与线程安全区别
- 常见锁概念
- 死锁
- 死锁四个必要条件
- 解决死锁问题
- 避免死锁算法
- Linux线程同步
- 条件变量
- 同步概念与竞态条件
- 条件变量函数
- 为什么`pthread_cond_wait`需要互斥量?
- 条件变量使用规范
作者:爱写代码的刚子
时间:2024.3.23
前言:本篇博客将会介绍线程的同步与互斥、可重入、线程安全、死锁的概念。
Linux线程分为用户级线程和内核的LWP
分离线程
- 默认情况下,新创建的线程是joinable的,线程退出后,需要对其进行pthread_join操作,否则无法释放资源,从而造成系统泄漏。
- 如果不关心线程的返回值,join是一种负担,这个时候,我们可以告诉系统,当线程退出时,自动释放线程资源。
int pthread_detach(pthread_t thread);
可以是线程组内其他线程对目标线程进行分离,也可以是线程自己分离:
pthread_detach(pthread_self());
分离后的线程是不能被pthread_join的(错误码22,表示非法参数)
分离线程可以由主线程或者线程自己来做(设置tid中相关的参数,表示该线程不是joinable的)
Linux线程互斥
进程线程间的互斥相关背景概念
- 尝试运行一段抢票代码:
#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include <cstdlib>
#include <vector>
using namespace std;
#define NUM 4
class threadData
{
public:
threadData(int number)
{
threadname = "thread-" + to_string(number);
}
public:
string threadname;
};
int tickets = 3000; // 用多线程,模拟一轮抢票
void *getTicket(void *args)
{
threadData *td = static_cast<threadData *>(args);
const char *name = td->threadname.c_str();
while (true)
{
if(tickets > 0)
{
usleep(1000);
printf("who=%s, get a ticket: %d\n", name, tickets); // ?
tickets--;
}
else{
break;
}
usleep(13); // 我们抢到了票,我们会立马抢下一张吗?其实多线程还要执行得到票之后的后续动作。usleep模拟
}
printf("%s ... quit\n", name);
return nullptr;
}
int main()
{
vector<pthread_t> tids;
vector<threadData *> thread_datas;
for (int i = 1; i <= NUM; i++)
{
pthread_t tid;
threadData *td = new threadData(i);
thread_datas.push_back(td);
pthread_create(&tid, nullptr, getTicket, thread_datas[i - 1]);
tids.push_back(tid);
}
for (auto thread : tids)
{
pthread_join(thread, nullptr);
}
for (auto td : thread_datas)
{
delete td;
}
return 0;
}
- 结果:
共享数据导致数据不一致问题,与多线程并发访问有关
寄存器不等于寄存器的内容
线程在执行的时候,将共享数据加载到CPU寄存器的本质:
把数据的内容,变成了自己的上下文——以拷贝的方式,给自己单独拿了一份
问题产生的原因:
分析ticket–
- 先将tickets读到cpu的寄存器中
- CPU内部进行–操作
- 将计算结果写回内存
其中每一步都会对应一条汇编操作
ticket-- => 1. mov[XXX] eax 2. – 3. mov eax [XXX]
每个线程都要执行这三步,同时任何时候线程都有可能被切换,同时线程也保存了硬件上下文
线程执行代码时根据这3条依次从上往下执行,而在这三条语句的任何地方,线程都有可能被切换走。CPU内的寄存器是被所有的执行流共享的,但是寄存器里面的数据是属于当前执行流的上下文数据。
线程被切换的时候,需要保存上下文
线程被换回的时候,需要恢复上下文
我们假设线程A在执行tickets–的任务,且tickets为10000。当tickets在寄存器已经计算一次完毕(tickets = 9999),准备将结果写回内存的时候,此时发生了线程切换(由线程A切至线程B),当前线程被拿下来了,此时寄存器里的值(9999)被放在了自己线程A的上下文里头,此时线程B也要执行tckets–的任务,且是不断循环此tickets–任务(读到寄存器,计算,返回结果),当tickets–到50的时候,再次–,读取寄存器,计算,-到49,准备写回的时候,线程B被切走了,保存自己的上下文数据,注意此时内存中tickets的数据已经是50了。线程A被切回来了,需要恢复上下文,把原先保存在线程A的值9999重新读回寄存器里,执行第三条语句:将计算完成的结果写回内存。此时内存中tickets由50变成了9999。我好不容易-到50的数据一瞬间回到解放前了!!!
上述就是典型的数据不一致问题!因为线程切换,多线程之间并发访问临界资源就会出现数据不一致的问题。上面的不只有–会出现数据不一致的问题,在判断tickets > 0时也同样会出现数据不一致:
我们假设tickets为1,此时线程A执行if判断,此步骤同样需要在cpu内的寄存器执行的,tickets = 1 > 0,判断后整准备返回结果时发生线程切换(由线程A切至线程B)
线程B也要执行if判断,把1从内存读到cpu里判断,发现tickets = 1 > 0,判断后返回结果到内存,随后执行tickets–语句,这里也要把tickets=1读到内存计算,计算后把结果0返回至内存。此时线程切换回至线程A,线程A继续执行未完成的tickets–语句,照样是把tickets = 0读到内存计算,计算后把结果-1返回至内存。票数只有1张,怎么可能出现这种情况呢。归根结底在于此判断发生了数据不一致问题!
能够出现数据不一致的问题本质还是线程切换过于频繁。而线程切换的场景如下:
时间片到了;线程会在内核返回到用户态做检测,创造更多的让线程阻塞的场景
- 临界资源:多线程执行流共享的资源就叫做临界资源
- 临界区:每个线程内部,访问临界资源的代码,就叫做临界区
- 互斥:任何时刻,互斥保证有且只有一个执行流进入临界区,访问临界资源,通常对临界资源起保护作用
- 原子性(后面讨论如何实现):不会被任何调度机制打断的操作,该操作只有两态,要么完成,要么未完成
互斥量mutex
- 大部分情况,线程使用的数据都是局部变量,变量的地址空间在线程栈空间内,这种情况,变量归属单个线程,其他线程无法获得这种变量。
- 但有时候,很多变量都需要在线程间共享,这样的变量称为共享变量,可以通过数据的共享,完成线程之间的交互。
- 多个线程并发的操作共享变量,会带来一些问题。
为什么可能无法获得争取结果?
- if 语句判断条件为真以后,代码可以并发的切换到其他线程
- usleep 这个模拟漫长业务的过程,在这个漫长的业务过程中,可能有很多个线程会进入该代码段
- –ticket 操作本身就不是一个原子操作(经过汇编转化为3条语句)
– 操作并不是原子操作,而是对应三条汇编指令:
- load :将共享变量ticket从内存加载到寄存器中
- update : 更新寄存器里面的值,执行-1操作
- store :将新值,从寄存器写回共享变量ticket的内存地址
要解决以上问题,需要做到三点:
- 代码必须要有互斥行为:当代码进入临界区执行时,不允许其他线程进入该临界区。
- 如果多个线程同时要求执行临界区的代码,并且临界区没有线程在执行,那么只能允许一个线程进入该临界区。
- 如果线程不在临界区中执行,那么该线程不能阻止其他线程进入临界区。
要做到这三点,本质上就是需要一把锁。Linux上提供的这把锁叫互斥量。
互斥量的接口
- 初始化互斥量
pthread_mutex_t是库给我们提供的一种数据类型
初始化互斥量有两种方法:
- 方法1,静态分配
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER//用于定义全局的锁,用了这把全局的锁之后就不需要销毁了
注意:局部的mutex不能采用该宏来初始化!!!
- 使用全局锁的示例:
- 方法2,动态分配
int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);
参数: mutex:要初始化的互斥量 attr:NULL
- 销毁互斥量
销毁互斥量需要注意:
- 使用 初始化的互斥量不需要销毁
- 不要销毁一个已经加锁的互斥量
- 已经销毁的互斥量,要确保后面不会有线程再尝试加锁
int pthread_mutex_destroy(pthread_mutex_t *mutex);
- 互斥量加锁和解锁
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
//返回值:成功返回0,失败返回错误号
调用pthread_mutex_lock
时,可能会遇到以下情况:
- 互斥量处于未锁状态,该函数会将互斥量锁定,同时返回成功
- 发起函数调用时,其他线程已经锁定互斥量,或者存在其他线程同时申请互斥量,但没有竞争到互斥量, 那么pthread_ lock调用会陷入阻塞(执行流被挂起),等待互斥量解锁。
只需要将临界资源(临界区加锁即可,加锁和解锁频繁也是会影响程序效率的)
-
加锁的本质:用时间来换取安全
-
加锁的表现:线程对于临界区代码串行执行
-
加锁原则:尽量的要保证临界区代码,越少越好!
#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include <cstdlib>
#include <vector>
using namespace std;
#define NUM 4
class threadData
{
public:
threadData(int number,pthread_mutex_t *mutex)
{
threadname = "thread-" + to_string(number);
lock = mutex;
}
public:
string threadname;
pthread_mutex_t *lock;
};
int tickets = 3000; // 用多线程,模拟一轮抢票
void *getTicket(void *args)
{
threadData *td = static_cast<threadData *>(args);
const char *name = td->threadname.c_str();
while (true)
{
pthread_mutex_lock(td->lock);//申请锁成功,才能往后执行,不成功,阻塞等待
if(tickets > 0)
{
usleep(1000);
printf("who=%s, get a ticket: %d\n", name, tickets); // ?
tickets--;
pthread_mutex_unlock(td->lock);//这里也要进行解锁
}
else{
pthread_mutex_unlock(td->lock);//注意这里一定要先解锁再跳出循环,不然该线程将会一直没有解锁
break;
}
usleep(13); // 我们抢到了票,我们会立马抢下一张吗?其实多线程还要执行得到票之后的后续动作。usleep模拟后续动作
}
printf("%s ... quit\n", name);
return nullptr;
}
int main()
{
pthread_mutex_t lock;
pthread_mutex_init(&lock,nullptr);
vector<pthread_t> tids;
vector<threadData *> thread_datas;
for (int i = 1; i <= NUM; i++)
{
pthread_t tid;
threadData *td = new threadData(i,&lock);
thread_datas.push_back(td);
pthread_create(&tid, nullptr, getTicket, thread_datas[i - 1]);
tids.push_back(tid);
}
for (auto thread : tids)
{
pthread_join(thread, nullptr);
}
for (auto td : thread_datas)
{
delete td;
}
pthread_mutex_destroy(&lock);
return 0;
}
- 线程对锁的竞争能力可能会不同(有的线程刚释放锁后又去申请锁了)
- 所以我们等线程释放锁后加上usleep
- 线程释放锁后不加usleep的后果:
- 不加
usleep
会导致已经抢到锁的线程刚释放锁但由于靠锁近,其他线程还没来得及唤醒竞争锁,该锁又被它抢走了。
纯互斥环境,如果锁分配不够合理(比如一个锁的竞争能力非常强),容易导致其他线程的饥饿问题!
但不是说只要有互斥,就必有饥饿,适合纯互斥的场景,就用互斥
- 所以我们可以定两条规则:
- 等待资源就绪的线程必须排队
- 刚释放锁的线程,不能立马重新申请锁,必须排队到队列的尾部
所以同步:让所有的线程按照一定的顺序获取锁和资源叫做同步
注意:锁本身也是一种共享资源!!!申请锁和释放锁本身就被设计成为了原子性操作
在临界区中,线程可以被切换吗?可以!在线程被切出去的时候,是持有锁被切走的,不是持有锁的线程就不能进入临界区访问临界资源!加锁能保证我当前线程在访问临界区期间对于其他线程来讲,是原子的(只有两种状态:持有锁和非持有锁,其他线程不关心持有锁线程里面的执行过程)
互斥量实现原理探究
【问题】:线程加锁和解锁具有原子性,原子性是如何实现的呢?
- 经过上面的例子,大家已经意识到单纯的 i++ 或者 ++i 都不是原子的,有可能会有数据一致性问题
- 为了实现互斥锁操作,大多数体系结构都提供了swap或exchange指令,该指令的作用是把寄存器和内存单元的数据相交换
- 由于只有一条指令,保证了原子性,即使是多处理器平台,访问内存的 总线周期也有先后,一 个处理器上的交换指令执行时另一个处理器的交换指令只能等待总线周期。
锁的原理
ticket——不是原子的,会变成3条汇编语句。原子:一条汇编语句就是原子的!
加锁的汇编语句:
以加锁示例,这是由多态汇编语句执行的,上述%al是寄存器,mutex就是内存中的一个变量。每个线程申请锁时都要执行上述语句,执行步骤如下:
- (movb $0,%al)先将al寄存器中的值清0。该动作可以被多个线程同时执行,因为每个线程都有自己的一组寄存器(上下文信息),执行该动作本质上是将自己的al寄存器清0。注意:凡是在寄存器中的数据,全部都是线程的内部上下文!多个线程看起来同时在访问寄存器,但是互不影响。
- (xchgb %al,mutex)然后用此一条指令交换al寄存器和内存中mutex的值,xchgb是体系结构提供的交换指令,该指令可以完成寄存器和内存单元之间数据的交换。
- 最后判断al寄存器中的值是否大于0。若大于0则申请锁成功,此时就可以进入临界区访问对应的临界资源;否则申请锁失败需要被挂起等待,直到锁被释放后再次竞争申请锁。
示例:假设内存中有一个变量mutex为1,cpu内部有%al寄存器,我们有threadA和threadB俩线程
- 现在线程A要开始加锁,执行上述语句。首先(movb $0,%al),线程A把0读进al寄存器(清0寄存器),然后执行第二条语句(xchgb %al,mutex),将al寄存器中的值与内存中mutex的值进行交换。
- 交换完成后,寄存器al的值为1,内存中mutex的值为0。此时这个过程就是加锁
- 当线程A争议执行第三条语句if判断时,发生了线程切换(切至线程B),但是线程A要把自己的上下文(1)带走。线程B也要执行加锁动作,同样是第一条语句把0加载到寄存器,清0寄存器。
- 随后线程B执行第二条语句交换动作,可是mutex的数据先前已经被线程A交换至寄存器,然后保存到线程A的上下文了,现在的mutex为0,而线程B执行交换动作,拿寄存器al的0去换内存中mutex的0。
- 随后线程B执行第三条语句if判断,可是我现在寄存器的值为0,判断失败,线程B挂起等待。此时线程B就叫做申请锁失败。
即使我线程A在执行第一条语句把寄存器清0后就发生了线程切换(切至线程B),线程A保存上下文数据(0),此时线程B执行第一条语句把0写进寄存器,随后线程B执行第二条语句xchgb交换:
当线程B好不容易拿到1将要进行if判断时,又发生了线程切换(切至线程A),线程B保留自己的上下文数据(1),线程A恢复上下文数据(0)到寄存器。随后线程A继续执行第二条xchgb交换语句,可是现在mutex为0啊,交换后寄存器的值依旧为0。
此时线程A执行第三条语句if判断失败,只能被挂起等待,线程A只能把自己的上下文数据保存,重新切换至线程B,也就是说我线程B只要不运行,你们其它所有线程都无法申请成功。线程B恢复上下文数据(1)到内存,然后执行第三条语句if成功,返回结果
注意:上述xchgb就是申请锁的过程。申请锁是将数据从内存交换到寄存器,本质就是将数据从共享内存变成线程私有。
-
mutex就是内存里的全局变量,被所有线程共享,但是一旦用一条汇编语句(原子)将内存的mutex值交换到寄存器,寄存器内部是哪个线程使用,那么此mutex就是哪个线程的上下文数据,那么就意味着交换成功后,其它任何一个线程都不可能再申请锁成功了,因为mutex已经独属于某线程私有了(交换成功后变成了线程的上下文,而线程的上下文是私有的,即把一个共享资源(临界资源)变成了私有资源)。
-
这个mutex = 1就如同令牌一般,哪个线程先交换拿到1,那么哪个线程就能申请锁成功,所以加锁是原子的
当线程释放锁时,需要执行以下步骤:
- 将内存中的mutex置回1。使得下一个申请锁的线程在执行交换指令后能够得到1,形象地说就是“将锁的钥匙放回去”。
- 唤醒等待Mutex的线程。唤醒这些因为申请锁失败而被挂起的线程,让它们继续竞争申请锁。
总结:
-
在申请锁时本质上就是哪一个线程先执行了交换指令,那么该线程就申请锁成功,因为此时该线程的al寄存器中的值就是1了。而交换指令就只是一条汇编指令,一个线程要么执行了交换指令,要么没有执行交换指令,所以申请锁的过程是原子的。
-
在线程释放锁时没有将当前线程al寄存器中的值清0,这不会造成影响,因为每次线程在申请锁时都会先将自己al寄存器中的值清0,再执行交换指令。
-
CPU内的寄存器不是被所有的线程共享的,每个线程都有自己的一组寄存器,但内存中的数据是各个线程共享的。申请锁实际就是,把内存中的mutex通过交换指令,原子性的交换到自己的al寄存器中。
把一个共享的锁,让一个线程以一条汇编的方式,交换到自己的上下文中,把锁变成了线程私有
解锁的汇编语句:
当然不一定只能同一个线程来申请锁和解锁,也可以一个线程加锁,一个线程来解锁,来实现两个线程的同步或者也可以避免死锁问题。
对锁进行封装(C++11lockguard锁)
#pragma once
#include <iostream>
class Mutex
{
public:
Mutex(pthread_mutex_t *lock):lock_(lock)
{}
void Lock()
{
pthread_mutex_lock(lock_);
}
void Unlock()
{
pthread_mutex_unlock(lock_);
}
~Mutex()
{}
private:
pthread_mutex_t *lock_;
};
class LockGuard
{
public:
LockGuard(pthread_mutex_t *lock):mutex_(lock)
{
mutex_.Lock();
}
~LockGuard()
{
mutex_.Unlock();
}
private:
Mutex mutex_;
};
- RAII思想的锁
可重入VS线程安全
概念
- 线程安全:多个线程并发同一段代码时,不会出现不同的结果。常见对全局变量或者静态变量进行操作,并且没有锁保护的情况下,会出现该问题。
- 重入:同一个函数被不同的执行流调用,当前一个流程还没有执行完,就有其他的执行流再次进入,我们称之为重入。一个函数在重入的情况下,运行结果不会出现任何不同或者任何问题,则该函数被称为可重入函数,否则,是不可重入函数(可能会出现线程安全问题)。
注意:线程安全和重入是两个概念!线程安全与不安全描述的是线程并发的问题,重入是描述函数的特点(没有褒贬之分,只是函数的特征)。我们现在接触的大部分函数都是不可被重入的(printf、scanf、文件操作、stl库)。
常见的线程不安全的情况
- 不保护共享变量的函数
- 函数状态随着被调用,状态发生变化的函数(如:使用static局部变量)
- 返回指向静态变量指针的函数(当一个线程正在访问静态变量时,另一个线程也可能在同时进行访问或修改,这样就有可能造成数据的不一致性或者未定义行为(竞态条件)。)
- 调用线程不安全函数的函数
- 会发生异常崩溃的函数
常见的线程安全的情况
- 每个线程对全局变量或者静态变量只有读取的权限,而没有写入的权限,一般来说这些线程是安全的
- 类或者接口对于线程来说都是原子操作
- 多个线程之间的切换不会导致该接口的执行结果存在二义性
常见不可重入的情况
- 调用了malloc/free函数,因为malloc函数是用全局链表来管理堆的
- 调用了标准I/O库函数,标准I/O库的很多实现都以不可重入的方式使用全局数据结构
- 可重入函数体内使用了静态的数据结构
常见可重入的情况
- 不使用全局变量或静态变量
- 不使用用malloc或者new开辟出的空间
- 不调用不可重入函数
- 不返回静态或全局数据,所有数据都有函数的调用者提供
- 使用本地数据,或者通过制作全局数据的本地拷贝来保护全局数据
可重入与线程安全联系
- 函数是可重入的,那就是线程安全的
- 函数是不可重入的,那就不能由多个线程使用,有可能引发线程安全问题
- 如果一个函数中有全局变量,那么这个函数既不是线程安全也不是可重入的。
可重入与线程安全区别
- 可重入函数是线程安全函数的一种
- 线程安全不一定是可重入的,而可重入函数则一定是线程安全的。
- 如果将对临界资源的访问加上锁,则这个函数是线程安全的,但如果这个重入函数若锁还未释放则会产生死锁,因此是不可重入的。
常见锁概念
死锁
- 死锁是指在一组进程中的各个进程均占有不会释放的资源,但因互相申请被其他进程所站用不会释放的资源而处于的一种永久等待状态。
死锁四个必要条件
- 互斥条件:一个资源每次只能被一个执行流使用 (最重要的前提)
- 请求与保持条件:一个执行流因请求资源而阻塞时,对已获得的资源保持不放(原则)
- 不剥夺条件:一个执行流已获得的资源,在末使用完之前,不能强行剥夺 (原则)
- 循环等待条件:若干执行流之间形成一种头尾相接的循环等待资源的关系(重要条件)
一旦产生了死锁,以上四个条件必须同时满足!
申请锁失败线程就会阻塞等待产生死锁(多次申请同一个锁会产生死锁)
解决死锁问题
理念:破坏4个必要条件,只需要一个不满足就可以的
方法:
- 请求与保持条件与不剥夺条件是可以通过函数来解决的
- 破坏循环等待条件:申请锁时按照一定的顺序,一个线程申请锁一、锁二,另一个线程申请锁一、锁二,不能一个线程申请锁一、锁二,另一个线程申请锁二、锁一
- 破坏死锁的四个必要条件
- 加锁顺序一致
- 避免锁未释放的场景
- 资源一次性分配
避免死锁算法
- 死锁检测算法(了解)
- 银行家算法(了解)
Linux线程同步
如何让线程实现排队?条件变量
条件变量
- 当一个线程互斥地访问某个变量时,它可能发现在其它线程改变状态之前,它什么也做不了。
- 例如一个线程访问队列时,发现队列为空,它只能等待,只到其它线程将一个节点添加到队列中。这种情况就需要用到条件变量。
- 条件变量要提供通知机制
- 要提供一个队列,能让线程在队列里排队(申请锁失败了就去排队)
- 条件变量需要被库管理
条件变量主要包括两个动作:
- 一个线程等待条件变量的条件成立而被挂起。
- 另一个线程使条件成立后唤醒等待的线程。
条件变量通常需要配合互斥锁一起使用。
- 我们学到的大部分概念,只要能创建多个的,都需要被管理起来。
同步概念与竞态条件
- 同步:在保证数据安全的前提下,让线程能够按照某种特定的顺序访问临界资源,从而有效避免饥饿问题,叫做同步(强调顺序性)
- 竞态条件:因为时序问题,而导致程序异常,我们称之为竞态条件。在线程场景下,这种问题也不难理解
有可能有人会想,既然线程都排队了,那同步问题为什么要带锁呢?其实这个问题是矛盾的,线程排队并不是自愿的,而是锁强迫的(有互斥的前提),访问资源失败了才回去排队。
条件变量函数
有关条件变量这篇博客也有介绍:
【C++11】线程库
初始化
int pthread_cond_init(pthread_cond_t *restrict cond,const pthread_condattr_t *restrict attr);
参数:
cond:要初始化的条件变量 attr:NULL
销毁
int pthread_cond_destroy(pthread_cond_t *cond)
定义全局的条件变量
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
等待条件满足
int pthread_cond_wait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex);
参数: cond:要在这个条件变量上等待 mutex:互斥量
唤醒等待
int pthread_cond_broadcast(pthread_cond_t *cond);//唤醒所有线程
int pthread_cond_signal(pthread_cond_t *cond);//唤醒一个线程
案例:
- 定义了一把锁和条件变量:
【问题】:为什么有时候线程向显示器输出的时候会出现混乱?
显示器也是文件,是临界资源,会被多线程进行竞争。
对打印函数进行加锁:
- 我们发现一个线程的竞争能力太大了,所以我们再次修改代码:
- &mutex_:让线程进行等待前,需要将锁进行释放,其他线程就会因为申请不了锁而产生死锁问题。所以pthread_cond_wait让线程等待的时候会自动释放锁!
- 如何唤醒线程?
- 使用
pthread_cond_broadcast
函数进行测试:
【问题】:我们怎么知道要让一个线程去休眠?
一定是临界资源不就绪,临界资源也是有状态的!
线程遇到临界资源不就绪走了就叫互斥,不走而是排队则叫同步!
【问题】:怎么知道临界资源是否就绪?判断是访问临界资源吗?
程序猿自己判断临界资源是否就绪,判断也是访问临界资源,所以判断必须在加锁之后!
所以pthread_cond_wait休眠函数必须在加锁和解锁之间(因为需要判断临界资源是否就绪,然后是否进行线程休眠),与之前pthread_cond_wait休眠之前需要释放锁相呼应!!!
为什么pthread_cond_wait
需要互斥量?
- 条件等待是线程间同步的一种手段,如果只有一个线程,条件不满足,一直等下去都不会满足,所以必须要有一个线程通过某些操作,改变共享变量,使原先不满足的条件变得满足,并且友好的通知等待在条件变量上的线程。
- 条件不会无缘无故的突然变得满足了,必然会牵扯到共享数据的变化。所以一定要用互斥锁来保护。没有互斥锁就无法安全的获取和修改共享数据。
- 按照上面的说法,我们设计出如下的代码:先上锁,发现条件不满足,解锁,然后等待在条件变量上不就 行了,如下代码:
// 错误的设计
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 。所以解锁和等待必须是一个原子操作。
- int pthread_cond_wait(pthread_cond_ t *cond,pthread_mutex_ t * mutex); 进入该函数后, 会去看条件量等于0不?等于,就把互斥量变成1,直到cond_ wait返回,把条件量改成1,把互斥量恢复 成原样。
条件变量使用规范
- 等待条件代码
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);
【附】:
小细节:不能对i取地址,因为可能会对后续循环中的i产生影响,要保证i独立