Linux -- 线程互斥与同步
一 线程互斥的概念
通常情况下,在使用线程时所涉及的数据大多属于局部变量范畴。这些数据项的内存空间主要位于栈内存区域,并且专有于单个线程,在其他线程无法获取这些资源的情况下展现出良好的独立性特点。然而,在某些情况下,则需要将大量数据进行跨进程共享以便完成进程间的通信与协作。当多个线程同时操作共享变量时会产生相应的问题
以下我们来模拟多线程票务抢订系统。通过一个全局变量 ticket 来记录当前可用座位数,并部署多个线程参与抢购行为。
#include<iostream>
#include<unistd.h>
#include<pthread.h>
#include<vector>
using namespace std;
#define NUM 5
int ticket = 100;
class threadData
{
public:
threadData(int number){
threadname = "thread-" + to_string(number);
}
public:
string threadname;
};
void* getTicket(void* args)
{
threadData *td = static_cast<threadData*>(args);
while(1)
{
if(ticket > 0)
{
usleep(1000); // 模拟抢票的时间
printf("%s is running, get a ticket: %d\n", td->threadname.c_str(), ticket);
ticket--;
}
else{
break;
}
}
return nullptr;
}
int main(){
vector<pthread_t> tids;
vector<threadData*> thread_datas;
for(int i = 1; i <= NUM; i++){
threadData* td = new threadData(i);
pthread_t tid;
pthread_create(&tid, nullptr, getTicket, td);
tids.push_back(tid);
thread_datas.push_back(td);
}
for(auto e : tids){
pthread_join(e, nullptr);
}
for(auto td : thread_datas){
delete td;
}
return 0;
}
我们运行起来之后,会看到线程抢到了负数的票!

那么为什么会发生这种情况呢?我们称这种现象为共享数据在无保护状态下的多线程并发访问导致的数据不一致问题。因此,在对单个全局变量进行多线程的增减操作时,并不保证操作的安全性。接下来我们将深入探讨这一问题的原因及解决方案。
为了实现 ticket- - 的完成过程,在汇编语言中遵循以下步骤:首先需要对 ticket- - 进行初始化操作。具体来说,在程序运行前必须调用初始化子程序,并将其参数设置为预设值以便后续使用。其次,在主程序运行期间需依次执行三条指令:第一条指令负责将 ticket 数据加载至CPU寄存器中;第二条指令则用于完成必要的计算操作;第三条指令则负责将 ticket 数据返回至内存空间中以供后续处理使用。这样一来,在完成上述所有操作后系统就能正确地执行相应的任务流程了
整个-- 过程如下:

假设现在运行两个线程:Line 1 和 Line 2,在它们之间的短暂空闲期中,系统随时可能切换当前运行的线程。当一个线程执行时,在其空闲期间将共享的数据加载到CPU寄存器的过程本质上是将这些数据内容转换为该特定上下文环境中的独立副本。这相当于通过复制的方式为自身独享一份数据副本。

当且仅当线程1 成功读取内存中的数据时(假设当前的数据值仍为100),才需要将其切换至另一个运行环境中)。在此时需要将该线程的上下文数据进行复制存储(其中核心操作是通过复制的方式获取一份独立的数据副本),以便在后续的操作中能够恢复原有的状态)。因此,在切换前,线程1 已经将当前的数据值(即100)复制并存储在其局部环境中。

随后进入运行状态的是线程2。在此时段内已获取大量‘ ticket’资源包,并且剩余的可用 tickets仅限于一张。

当线程2被切换时,在完成操作后,“线程1”会将它的上下文数据返回到CPU进行计算。需要注意的是,在这个过程中,“线程1”的计数器值仍为100。计算完成后变为99,并立即将其值写入内存。这种操作引发了数据一致性问题,并非安全操作。也就是说,“ticket- -”操作缺乏原子性保障

此外, 我们不仅还在进行数值运算(ticket- -), 同时也在对ticket进行大于0的判断, 这一过程同样涉及对ticket值的进一步计算. 即进行逻辑运算. 假设当前ticket值为1, 那么在判断过程中可能会导致多个线程同时参与判断. 因为一个线程在其执行判断任务的过程中有可能会被调度资源分配到其他任务. 这使得每个等待执行的任务都会认为当前ticket值依然是1, 因此会自动将该值减小至负数. 而当所有相关判断任务完成后, 将不会有新的操作对该值产生影响, 因此后续的所有计算都必须重新从内存中读取最新的ticket值.
那么如何应对这一问题?对于共享数据的访问,“必须确保只有一个执行流程同时访问”,这体现了互斥机制!因此我们需要采用一种称为mut exclusive lock的技术来实现这一点。接下来我们就开始学习这种技术。
二 互斥锁
在Linux系统中,pthread库为我们提供了解决多线程访问共享数据不一致问题的互斥锁机制
互斥锁即是为一种用于控制多线程并发访问资源机制的技术手段我们通过使用互斥锁来管理一组需要竞争同一资源的多线程系统使其中任意时刻只允许一个线程执行对该资源的操作从而保证系统的安全性与稳定性
接下来我i们认识一下互斥锁的相关接口:
2.1 锁的初始化
pthread_mutex_init()
#include <pthread.h>
int pthread_mutex_init(pthread_mutex_t *restrict mutex,
const pthread_mutexattr_t *restrict attr);
此接口用于对一把锁进行初始化。此库中第一个参数的成员变量是 pthread_mutex_t ,它表示一种数据类型是我们提供的。其中一种数据类型是我们提供的。
第一个参数属于输入型参数,并且我们需要为此目的创建一个 pthread_mutex_t 类型的锁并传入其地址以实现功能
第二个参数代表这把锁的属性,我们也不管,设置为 nullptr 即可。
实际上,在初始化一把锁时存在两种不同的方法:一种是在内部直接完成这一操作;其二是通过调用库函数来实现全局锁定。如果采用上述方法来设定一把全局锁,则就不会再需要调用前面的方法;但即使不释放也没问题。值得注意的是,在这种情况下...
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
2.2 锁的释放
pthread_mutex_destroy()
#include <pthread.h>
int pthread_mutex_destroy(pthread_mutex_t *mutex);
此接口的功能是实现对一把锁的释放。此接口的首个参数与初始化过程中的首个参数相同。请注意,在采用线程安全互斥锁机制(即调用函数 pthread_mutex_init())进行互斥锁管理时,则必须确保随后调用函数 pthread_mutex_destroy() 来完成相应的解锁操作;而如果采用全局锁机制,则无需手动进行资源管理。
2.3 加锁
pthread_mutex_lock()
#include <pthread.h>
int pthread_mutex_lock(pthread_mutex_t *mutex);
该接口就是对一把锁进行加锁(也就是对该锁进行使用,上锁)
2.4 解锁
#include <pthread.h>
int pthread_mutex_unlock(pthread_mutex_t *mutex);
该接口就是对一把锁进行解锁,也就是解除对资源的锁定。
2.5 使用示例与说明
基于之前的抢票代码,我们对该 ticket 进行加锁操作。通过此操作,该 ticket 成为了一个临界资源。实际上,并非所有代码都会在此时进行访问,而是仅有少部分代码会在此时进行访问。因此,在这种情况下,我们将其定义为一个临界区。
其实加锁的本质是一种通过时间差换取安全机制的方法。从实际操作角度来看,在一个并发环境中当一个线程执行临界区代码时必须等待其他线程完成之前的操作。因此,在上述代码中,默认情况下将被保护的区域定义为对 ticket 变量进行读写的区域。以下是对该区域的具体描述:
首先我们在类中定义一把锁,方便每个线程都有自己的锁:
class threadLock
{//我们将锁的id和锁姓名封装成一个类
public:
std::string _name; // 给程序员识别的
pthread_mutex_t* _lock; //让不同的线程使用的锁
public:
threadLock(int num,pthread_mutex_t* Lock)
:_lock(Lock)
{
_name="Thread"+std::to_string(num);
}
};
在 main() 函数体内设置了互斥锁;需要注意的是,在主线程层面设置了该互斥锁,并且考虑到我们的抢票逻辑也在 main() 函数体内运行的情况下,在整个程序执行流程中都确保会正确地进行资源管理;最后应该会在 main() 结束之前都会释放该互斥锁以避免潜在的资源泄漏问题;代码实现部分将在下方展示
int main(){
pthread_mutex_t lock; //定义一把锁
pthread_mutex_init(&lock,nullptr); //进行锁的初始化
std::vector<pthread_t> tids; //线程编号数组 这两组是为了方便释放
std::vector<threadLock*> thread_locks; //锁编号数组
for(int i=0;i<=NUM;i++){ //循环创建线程,进行抢票
threadLock* td=new threadLock(i,&lock); //创建一个新锁
pthread_t tid;
pthread_create(&tid, nullptr, getTicket, td);//创建线程 同时抢票
//让线程和锁进入不同的数组
tids.push_back(tid);
thread_locks.push_back(td);
}
//循环释放
for(auto e : tids){
pthread_join(e, nullptr);
}
for(auto td : thread_locks){
delete td;
}
//删除锁
pthread_mutex_destroy(&lock);
return 0;
}
接下来是抢票的程序:
void* getTicket(void* args)
{
//获取锁名和id
threadLock *td = static_cast<threadLock*>(args);
while(1){ //循环抢票
//上锁 每一次我们将要访问下面这段代码时,除了当前线程,禁止其它线程访问
pthread_mutex_lock(td->_lock);
if(ticket > 0){//有票
usleep(1000); // 模拟抢票的时间
printf("%s is running, get a ticket: %d\n", td->_name.c_str(), ticket);
ticket--;
pthread_mutex_unlock(td->_lock); //关锁
usleep(10);
}
else{ //无票 关锁 退出
pthread_mutex_unlock(td->_lock);
break;
}
//pthread_mutex_unlock(td->_lock); //在这里不如上面分两端写,两端写锁占用的资源更少
}
std::cout << td->_name.c_str() << "` quit!" << std::endl;
return nullptr;
}
头文件及杂项:
#include<iostream>
#include<pthread.h>
#include<string>
#include<vector>
#include<time.h>
#include<unistd.h>
const int NUM=5; //创建的线程数量
int ticket=10000; //票数
如上代码中,在加锁与解锁操作所限定的区域中,则被统称为一个临界区(Critical Section)。其中,在请求锁的过程中若能够成功获取到锁,则后续的操作就能够顺利进行;但如果无法获得锁,则会导致执行被阻塞等待!
执行的结果如下:

我们能够观察到,在使用 ticket 时不会出现 0 或负数的情况,并且这表明临界资源在被多个进程并发访问时导致的数据不一致性问题已经得到了解决。
但是代码中有些细节我们还需要讲解一下。
- 在抢票的过程中,在一个线程抢完票后解锁后,在其后面添加了一条
usleep(10);这意味着当一个线程加锁时其他线程会被阻塞等待挂起,在该线程解锁时其他线程还没有从阻塞状态转变为运行状态的情况下再次加锁就会付出更高的代价;而且我们抢完票之后还有后续的操作需要执行比如处理抢到票后的相关事务这里我们却没有做到因此我们在一个线程解锁之后添加短暂的时间休眠一是为了给其他线程腾出时间进行唤醒二是为了模拟抢到票后的后续操作。 - 如果我们在代码中没有添加
usleep(10);那么该线程就会一直占据这把锁导致票被它抢完这就是典型的互斥环境下的饥饿问题这种情况下如果锁分配不够合理确实会导致其他线程长时间得不到资源而陷入等待状态最终导致进程因饥饿而死亡这就是所谓的饥饿问题。 - 对于新加入的线程来说它们必须按照从队列尾部开始依次排队;而对于已经解锁的线程来说它们也必须按照从队列尾部开始依次排队这样就可以保证所有参与竞争的线程都能按照一定的顺序获取资源这种按照一定顺序进行资源获取的方式称为同步机制这是我们后面将重点探讨的内容。
- 每个进入临界区进行操作的线程都需要首先申请加锁以获得共享资源即临界资源所以加锁和解锁操作本身就被设计成了原子性的操作!如何实现这一点呢?我们将在后面的章节中详细讲解其中的道理。
- 在临界区内部是否可以对当前运行的线程进行切换呢?答案是可以!这是因为当一条被切换出去的线程在执行加锁操作时会伴随着自身状态一起被切换离开所以在该条线索被切换出去的过程中其他所有在线等待中的线程都无法进入临界区访问资源因为共享锁只能被一条线路占用!因此对于所有未被调度执行的任务来说它们要么没有取得锁要么正在释放锁当前正在执行加锁操作的状态下的这条线索访问临界区的过程对于所有其他的线索而言就是一个不可分割的整体动作!
三 锁的原理
我们都知道 ticket >0 不是原子操作。这是因为该操作会被分解为三个汇编指令。那么什么属于原子操作呢?在底层编程模型中,我们认为一条汇编指令就是原子操作。
为了该方法旨在实现互斥锁操作

首先 movb 就是把 0 写入 al 寄存器,可以理解为 eax 寄存器:

在操作中 xchgb 实现了对 al 寄存器内容与内存中 mutex 变量之间的数值交换作用;其中 mutex 初始化为1

接下来检测 al 寄存器中的值是否超过0,并表明申请加锁成功与否的结果;如果无法实现加锁,则导致进程挂起并等待复用。
在之前的演示中,我们展示了一个单线程来进行加锁操作。那么如果同时有两个线程试图进行加锁操作呢?举个例子来说吧——比如说两个不同的线程A和B试图进行加锁操作。然而核心问题在于一段代码,在编译过程中会被分解成多个汇编指令。因此,在执行到某个汇编指令时——就有可能被操作系统调度切换到下一个执行位置。
假设正在处理线程1的加锁请求时,
刚完成第一步操作,
将数值0写入到al寄存器中,
实际上是在该线程的硬件环境中进行操作。
随后另一个线程(比如线程2)进入此操作环境,
此时需要将当前正在执行的操作从主线路上切换到另一个进程,
因此需要立即保存当前寄存器中的状态信息,
即将当前存储的内容记录下来,
当恢复到主线时会自动触发xchgb指令进行重定位。

当线程2到来时,在到达之前先将零重设至 al 寄存器中。随后执行一个名为xchgb的操作,在完成一次数据交换操作后(即将 al 寄存器中的内容与内存内容互换),会使得后续操作能够顺利进行。在这种情况下(即操作完成之后),如果检查发现 al 寄存器中的值大于零,则表示主程序流程已经进入下一个阶段(即可以直接返回)。

在执行线程2的判断时,在恢复执行前需确保上下文环境已正确复原;随后需将AL寄存器的内容归零以完成状态复原的过程;随后与内存进行数据交换;交换完成后通过比较发现AL寄存器仍保持低值状态;此时系统会暂停当前操作任务并等待相应的处理结果。

在等待期间不被调度,在此期间将会有另一个进程(如线程2)重新引入以恢复上下文。随后将目标值放入al寄存器中,并进行判断以确定其值是否大于零。如果满足条件,则会成功获得加锁权限并返回值零。
通过上述过程可以看出,其中最核心的是 xchgb 语句的作用。交换的核心在于将内存中的数据转移至CPU的寄存器中,即为将数据传递至当前线程的工作空间!这意味着每个线程都有独立的上下文空间。然而内存空间是所有线程共享使用的,并且锁仅有一个实例。因此当一个线程申请加锁时,它实际上将唯一的共享锁转换为其专用的寄存器空间。
那么解锁的汇编语句如下:

实际上就是将内存中的数据设置为1就可以。不需将其上一次线程申请加锁的标志位交换回内存中,因为这并非必要操作,每个线程在执行加锁操作时都必须先将自己的状态标记为0,也就是直接记录到自己的硬件环境中,这也就意味着之前已经加锁过的状态已经被覆盖了。
四、可重入和线程安全
4.1 概念
- 线程安全:当多个线程同时执行同一段代码时,在没有互斥机制保护的情况下不会导致不同的运行结果;常见的操作包括对全局变量或静态变量的操作。
- 通常会对全局变量或静态变量进行操作,在没有互斥机制保护的情况下会出现该问题;
- 重入:同一个函数被不同的执行路径调用;当其中一个执行路径尚未完成时我们称之为重入。
- 我们称之为重入;
- 一个函数在重入的情况下运行的结果不会有差异或问题则称其为可重入函数;
- 否则不可称为可重入函数。
这表明,在一个函数无法被重入的情况下,在多线程环境中运行时可能会导致线程安全问题。而如果一个函数是可以被重入的,则绝对不可能存在线程安全问题。
4.2 可重入与线程安全联系
- 函数允许多个线程调用,则该函数是无危险地被多个线程调用;
- 函数不能允许多个线程调用,则该函数不能由多个线程使用,并且可能会导致 thread safety 问题;如果一个函数中含有 global variables,则该函数既不是 thread-safe 也不是 reentrant。
4.3 可重入与线程安全区别
- 可重入类函数属于线程安全函数的一种类型;
- 并非所有线程安全功能都是可重入的;然而所有可重入功能都是线程安全的;
- 当对该临界资源加锁时,则该程序是非阻塞状态下的正常运行状态;即该程序在此状态下是无竞争且互斥的安全性的实现保证;
- 而如果未释放相应的锁定机制可能导致死锁现象发生,则该程序不具备可重入性。
最后总结就四个结论:
- 线程安全关注的是并发操作的执行顺序
- 可重入特性反映的是函数在其参数空间内的行为一致性
- 不可重入函数在多 thread 访问时可能导致 thread 安全问题
- 可重入函数在多 thread 访问时不会导致 thread 安全问题
五、死锁
5.1.死锁概念
死锁被称为一种现象,在多线程环境中出现时被描述为:当一个线程持有某个互斥资源而另一个线程试图获得同一资源时,并不释放自己的资源而导致的一种永久等待状态
5.2 死锁的必要条件
请阐述一下什么是导致死锁发生的必要条件?即一旦出现死锁现象,则必须全部四个相关条件都已得到满足。
- 互斥机制:该系统设计规定每个资源仅能被单一执行流程所使用
- 请求与保持机制:当某执行流程因申请所需资源而停滞时,则必须优先保障其已获 resource 的持续可用性
- 不剥夺原则:任何已进入某 resource 的 user,在未耗尽全部 resource 资源之前不得无故取消其 access 权利
- 循环依赖关系:多个独立的 user group 之间相互制约,在 resource 分配过程中形成首尾相连的循环依赖关系
5. 3. 避免死锁
- 破坏死锁的四个重要条件之一
- 加锁操作具有可重复性
- 确保资源被正确释放
- 资源分配应一次性完成
六 线程同步
6.1 线程同步的基本概念
同步:确保数据的安全性,并通过按特定顺序安排线程访问关键资源来有效地防止饥饿现象的发生。
我们举个例子理解一下这段话:
假设我们的学校的自习室较为老旧,在校内设置了仅通过持有特殊磁卡方能使用的制度。张三同学于早上5:30提前锁定了唯一一间空闲的自习室,并独享了这一间用于学习的时间(例子需要)。
此时必定有 someone 想进入自习室,然而因为没有 key,所以不得不在自习室外长时间等待(即陷入僵局).然而张三却不着急,慢慢悠悠地在自习厅度过中午时光,这时他感到有些饥饿感,想要出去觅食一餐,而吃晚饭则意味着必须归还钥匙(这是规定).
正常人在一般情况下就会去吃饭,但张三是个死不活之徒,他暗自思忖道:要是我去吃饭,岂不是也得排很长时间的队?
于是法外狂徒张三决定暂时不吃饭,并开始潜心学习。刚进入自习室仅几分钟后,张三便感到强烈的饥饿感促使他再次离开。他心切地出去觅食,在归还钥匙时发现有诸多同学聚集在此感到遗憾咬着牙再次拿回钥匙投入学习
到了自习室被强行锁门的时候之后他去了食堂并且准备挑战夜晚的怪物猎人任务他选择了长剑并成功地击败了一只怪兽完全忽视了外界有人 kill 的目光
张三错了吗?张三没错,十分符合自习室的规定,只是 不合理。
自习室内的资源被不必要的浪费所消耗,在此环境中学习的张三同学因饥饿而感到不安,并未能有效地进行学习。
在外等待的同学更是消磨了自己的时间,活生生被张三”饿死".
为此校方更新了 自习室 的规则:
- 每位学习完毕的同学在归还钥匙后不可以立即再次提交申请
- 外侧等待借还钥匙的同学需依次排队等候并按流程执行
制度更新后就不会出现这种 饥饿问题 了;因此解决 饥饿问题 的关键是:在保证资源的安全性下实施有序访问机制。
即通过 线程同步 解决 饥饿问题
6.2 条件变量
原生线程库 中提供了 条件变量 这种方式来实现 线程同步
条件机制是基于线程间的共享全局变量实现同步的一种方法。该机制主要包括两个核心操作:
- 一个线程因等待相关变量满足其运行条件而被阻塞,
- 另外一条线程触发其运行条件并发送 wake-up 信号以唤醒等待中的线程。
例如当一个线程访问队列时发现该队列为空则必须等待直至其他线程将数据放入该队列中之后才可考虑采用条件变量
条件变量的本质就是 衡量访问资源的状态
竞态条件:因为时序问题而导致程序出现异常
我们可以把 条件变量 视为一个结构体,
其中包含着 队列 结构,
用于存储当前等待进站的 线程信息。
当条件得到满足时会取出队列前端的 线程来进行操作,
操作完成后会将取出的 线程重新插入到队尾位置上。
6.3 同步相关操作
6.3.1 条件变量的创建与销毁
作为 原生线程库 中的 条件变量 ,它采用与互斥锁类似的接口设计。例如,在C语言中,其类型定义为pthread_cont_t$。同样,在创建后必须进行初始化操作。
#include <pthread.h>
pthread_cond_t cond; // 定义一个条件变量
int pthread_cond_init(pthread_cond_t *restrict cond,
const pthread_condattr_t *restrict attr);
//参数1 pthread_cond_t* 表示想要初始化的条件变量
//第二个参数是pthread_condattr_t类型的指针变量,在初始化阶段表示相关的配置属性;通常情况下,默认值会被指定为nullptr以采用系统的默认配置
//返回值:成功返回 0**,失败返回** error number
//条件变量 在使用结束后需要销毁
#include <pthread.h>
int pthread_cond_destroy(pthread_cond_t *cond);
pthread_cond_t* 表示想要销毁的条件变量
返回值:成功返回 0**,失败返回** error number
注:如同互斥锁一样,条件变量支持静态分配的方式,在创建全局条件变量时被定义为**PTHREAD_COND_INITIALIZER**的类型变量名(缩写形式),该类型变量名表示自动生成初始化和销毁操作的能力。
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
同时也要注意,这种初始化只适用于全局变量。
6.2.2 条件等待
#include <pthread.h>
int pthread_cond_wait(pthread_cond_t *restrict cond,
pthread_mutex_t *restrict mutex);
参数1 pthread_cond_t* 想要加入等待的条件变量
参数2 pthread_mutex_t* 互斥锁,用于辅助条件变量
返回值:成功返回 0**,失败返回** error number
关于参数2的详细说明,请注意以下要点:首先,请明确条件变量在使用时必须与互斥锁配合使用;其次,在获取锁资源之后,并且通过条件变量来判断条件是否满足。
传递互斥锁的理由:
- condition variables, too, are critical resources that must be protected.
- When the condition is not met (i.e., not being awakened), the thread currently holding the lock will be blocked (hung up), while other threads are still waiting for access to the lock resource. To prevent deadlock situations (deadlocks), condition variables must possess the ability to automatically release locks (autorelease capability). In other words, in such cases, releasing the lock resource directly would suffice.(lock is also a resource)
当特定线程被唤醒时, 条件变量会释放锁, 并使该线程进入 条件等待 状态
6.2.3 唤醒线程
在条件变量内部运行的线程如果没有被唤醒,则无法判断队头线程。可以通过调用pthread_cond_signal函数来唤醒相关线程(从而使条件变量变为true)。
#include <pthread.h>
int pthread_cond_signal(pthread_cond_t *cond);
pthread_cond_t 表示想要从哪个条件变量中唤醒线程
返回值:成功返回 0**,失败返回** error number
注意: 使用 pthread_cond_signal 一次只会唤醒一个线程,即队头线程
若想唤醒全部线程(即唤醒所有条件变量),则可通过parched_cond_broadcast来实现
#include <pthread.h>
int pthread_cond_broadcast(pthread_cond_t *cond);
数和返回值意义与前者相同,在代码中使用的 broadcast 实际上即为广播机制,在这种机制下会逐个通知该 条件变量 中的所有线程以访问 临界资源。
6.4 测试代码
接下来简单使用一下 线程同步 相关接口
目标:创建 5 个次线程,等待条件满足,主线程负责唤醒
#include <iostream>
#include <pthread.h>
#include <unistd.h>
using namespace std;
// 互斥锁和条件变量都定义为自动初始化和释放
pthread_mutex_t mtx = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
const int num = 5; // 创建五个线程
void* Active(void* args){
const char* name = static_cast<const char*>(args);
while(true){
// 加锁
pthread_mutex_lock(&mtx);
// 等待条件满足
pthread_cond_wait(&cond, &mtx);
cout << "\t线程 " << name << " 正在运行" << endl;
// 解锁
pthread_mutex_unlock(&mtx);
}
delete[] name;
return nullptr;
}
int main()
{
pthread_t pt[num];
for(int i = 0; i < num; i++){
char* name = new char[32];
snprintf(name, 32, "thread-%d", i);
pthread_create(pt + i, nullptr, Active, name);
}
// 等待所有次线程就位
sleep(3);
// 主线程唤醒次线程
while(true) {
cout << "Main thread wake up Other thread!" << endl;
pthread_cond_signal(&cond); // 单个唤醒
sleep(1);
}
for(int i = 0; i < num; i++)
pthread_join(pt[i], nullptr);
return 0;
}

