文章目录
- 5. Linux线程互斥
- 5.1 进程线程间的互斥相关背景概念
- 5.2 互斥量mutex
- 5.2.1 互斥量的接口
- 5.2.2 销毁互斥量
- 5.2.3 互斥量加锁和解锁
- 5.3 互斥量实现原理探究
- 5.4 互斥量的封装
- 6. 可重入VS线程安全
- 6.1 常见的线程不安全的情况:
- 6.2 常见的线程安全的情况
- 6.3 常见不可重入的情况
- 6.4 常见可重入的情况
- 6.5 可重入与线程安全联系
- 6.6 可重入与线程安全区别
- 7. 常见锁概念
- 7.1 死锁
- 7.2 死锁的四个必要条件
- 7.3 避免死锁
- 7.4 避免死锁算法
- 8. 线程同步
- 8.1 条件变量
- 8.2 同步概念与竞态条件
- 8.3 条件变量函数
5. Linux线程互斥
5.1 进程线程间的互斥相关背景概念
- 临界资源:多线程执行流共享的资源就叫做临界资源
- 临界区:每个线程内部,访问临界资源的代码,就叫做临界区
- 互斥:任何时刻,互斥保证有且只有一个执行流进入临界区,访问临界资源,通常对临界资源起保护作用
- 原子性(后面讨论如何实现):不会被任何调度机制打断的操作,该操作只有两态,要么完成,要么未完成
5.2 互斥量mutex
- 大部分情况,线程使用的数据都是局部变量,变量的地址空间在线程栈空间内,这种情况,变量归属单个线程,其他线程无法获得这种变量。
- 但有时候,很多变量都需要在线程间共享,这样的变量称为共享变量,可以通过数据的共享,完成线程之间的交互。
- 多个线程并发的操作共享变量,会带来一些问题。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>// 共享的全局变量,表示剩余票数
// 因为是全局变量,所有线程都可以访问和修改它
int ticket = 100;// 售票窗口(线程)的工作函数
void *route(void *arg)
{char *id = (char*)arg; // 转换参数为字符串,表示窗口编号while ( 1 ) {if ( ticket > 0 ) { // 还有票可卖// 模拟售票过程耗时usleep(1000); // 休眠1000微秒// 打印售票信息: 窗口号和售出的票号printf("%s sells ticket:%d\n", id, ticket);ticket--; // 售出一张票,票数减1// 注意: 这里存在竞态条件(race condition)// 因为多个线程同时操作ticket变量// 可能导致超卖或重复售票的问题} else { // 票已售完break; // 退出循环}}
}int main( void )
{// 创建4个线程代表4个售票窗口pthread_t t1, t2, t3, t4; // 存储4个线程的线程ID// 创建4个售票线程// 每个pthread_create的第4个参数传入不同的窗口编号字符串pthread_create(&t1, NULL, route, (void*)"thread 1");pthread_create(&t2, NULL, route, (void*)"thread 2");pthread_create(&t3, NULL, route, (void*)"thread 3");pthread_create(&t4, NULL, route, (void*)"thread 4");// 等待所有售票线程结束pthread_join(t1, NULL);pthread_join(t2, NULL);pthread_join(t3, NULL);pthread_join(t4, NULL);
}
执行结果:
thread 4 sells ticket:100
...
thread 4 sells ticket:1
thread 2 sells ticket:0
thread 1 sells ticket:-1
thread 3 sells ticket:-2
这段代码模拟了一个售票系统:
- 系统总共有
100
张票- 有
4
个窗口(线程)同时售票- 存在严重的并发问题:
- 多个线程同时读写
ticket
变量- 检查
ticket
和减少ticket
之间有时间差- 可能导致超卖或重复售票
- 这种问题的解决方案是:
- 使用互斥锁(
mutex
)保护临界区- 使用原子操作
- 使用其他同步机制
为什么可能无法获得争取结果?
if
语句判断条件为真以后,代码可以并发的切换到其他线程usleep
这个模拟漫长业务的过程,在这个漫长的业务过程中,可能有很多个线程会进入该代码段--ticket
操作本身就不是一个原子操作
tickets--
操作并不是原子操作,而是对应三个步骤:
先将
tickets
读入到cpu
的寄存器中
CPU
内部进行--
操作将计算结果写回内存
--
对应三条汇编指令:
load
:将共享变量ticket
从内存加载到寄存器中update
: 更新寄存器里面的值,执行-1
操作store
:将新值,从寄存器写回共享变量ticket
的内存地址
要解决以上问题,需要做到三点:
- 代码必须要有互斥行为:当代码进入临界区(是指在多线程环境下,访问共享资源的代码片段)执行时,不允许其他线程进入该临界区。
- 如果多个线程同时要求执行临界区的代码,并且临界区没有线程在执行,那么只能允许一个线程进入该临界区。
- 如果线程不在临界区中执行,那么该线程不能阻止其他线程进入临界区。要做到这三点,本质上就是需要一把锁。
Linux
上提供的这把锁叫互斥量。
5.2.1 互斥量的接口
初始化互斥量有两种方法:
方法1,静态分配:
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER
pthread_mutex_t mutex; // 互斥锁变量声明
PTHREAD_MUTEX_INITIALIZER // 静态初始化宏
方法2,动态分配:
int pthread_mutex_init(pthread_mutex_t *restrict mutex, const
pthread_mutexattr_t *restrict attr);
参数:mutex:要初始化的互斥量attr:NULL
5.2.2 销毁互斥量
销毁互斥量需要注意:
- 使用
PTHREAD_MUTEX_INITIALIZER
初始化的互斥量不需要销毁- 不要销毁一个已经加锁的互斥量
- 已经销毁的互斥量,要确保后面不会有线程再尝试加锁
int pthread_mutex_destroy(pthread_mutex_t *mutex);
参数:pthread_mutex_destroy // 函数名:销毁互斥锁(pthread_mutex_t *mutex) // 参数:互斥锁指针
pthread_mutex_destroy
的主要作用是释放与互斥锁相关的系统资源,防止资源泄漏。
class ResourceManager {
private:pthread_mutex_t mutex;bool is_initialized;public:ResourceManager() {// 初始化互斥锁if (pthread_mutex_init(&mutex, NULL) != 0) {is_initialized = false;throw runtime_error("Mutex init failed");}is_initialized = true;}~ResourceManager() {if (is_initialized) {// 不再使用时销毁互斥锁,释放系统资源pthread_mutex_destroy(&mutex);}}
};
5.2.3 互斥量加锁和解锁
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
返回值:成功返回0,失败返回错误号
互斥锁使用示例:
// 静态初始化方式
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;void* thread_function(void* arg) {pthread_mutex_lock(&mutex); // 加锁// 临界区代码pthread_mutex_unlock(&mutex); // 解锁return NULL;
}
调用
pthread_lock
时,可能会遇到以下情况:
- 互斥量处于未锁状态,该函数会将互斥量锁定,同时返回成功
- 发起函数调用时,其他线程已经锁定互斥量,或者存在其他线程同时申请互斥量,但没有竞争到互斥量,那么
pthread_lock
调用会陷入阻塞(执行流被挂起),等待互斥量解锁。
加锁的本质:是用时间来换取安全
加锁的表现:线程对于临界区代码串行执行
加锁原则:尽量的要保证临界区代码,越少越好
临界区特点:
- 一次只允许一个线程进入
- 线程必须按照特定顺序获取资源
- 在临界区内的线程被切出时,其他线程仍无法进入
锁的双重身份:
锁本身就是一个共享资源
同时它又是用来保护其他共享资源的工具
锁操作的特殊性:
申请锁(lock)和释放锁(unlock)这两个操作本身必须是原子性的
为什么?因为如果锁的操作不是原子性的,那么保护共享资源的机制本身就会失效
想象一个场景:
锁就像一个门的钥匙
钥匙本身就是大家都想要的资源
"拿钥匙"和"放回钥匙"这个动作必须是一气呵成的
不能在拿钥匙的过程中被打断,否则可能出现多人同时拿到钥匙的混乱情况
设计原则:
锁的申请和释放操作被刻意设计成原子性操作
这是在硬件层面就保证的特性
正是因为这个特性,才能保证锁机制的可靠性
锁本身就是共享资源。所以,申请锁和释放锁本身就被设计成为了原子性操作
在临界区中,线程可以被切换吗?
可以切换。在线程被切出去的时候,是持有锁被切走的。我不在期间,照样没有人能进入临界区访问临界资源
对于其他线程来讲,一个线程要么没有锁,要么释放锁
当前线程访问临界区的过程,对于其他线程是原子的。
改进上面的售票系统:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
#include <sched.h>// 共享的全局变量,表示剩余票数
int ticket = 100;// 定义互斥锁,用于对临界区进行保护
pthread_mutex_t mutex;// 售票窗口(线程)的工作函数
void *route(void *arg)
{char *id = (char*)arg; // 转换参数为字符串,表示窗口编号while ( 1 ) {// 在访问共享资源前加锁pthread_mutex_lock(&mutex);if ( ticket > 0 ) { // 还有票可卖usleep(1000); // 模拟售票过程耗时printf("%s sells ticket:%d\n", id, ticket);ticket--; // 售出一张票,票数减1// 完成票务操作后释放锁pthread_mutex_unlock(&mutex);// sched_yield(); // 可选的让出CPU时间片// 让其他线程有更多机会执行} else { // 票已售完// 记得在break前释放锁,否则会造成死锁pthread_mutex_unlock(&mutex);break;}}
}int main( void )
{pthread_t t1, t2, t3, t4; // 存储4个线程的线程ID// 初始化互斥锁// 第二个参数为NULL表示使用默认属性pthread_mutex_init(&mutex, NULL);// 创建4个售票线程pthread_create(&t1, NULL, route, "thread 1");pthread_create(&t2, NULL, route, "thread 2");pthread_create(&t3, NULL, route, "thread 3");pthread_create(&t4, NULL, route, "thread 4");// 等待所有售票线程结束pthread_join(t1, NULL);pthread_join(t2, NULL);pthread_join(t3, NULL);pthread_join(t4, NULL);// 销毁互斥锁,释放资源pthread_mutex_destroy(&mutex);
}
这是修复了并发问题的售票系统:
- 使用
pthread_mutex_t
定义了互斥锁- 在操作共享资源
ticket
前加锁(pthread_mutex_lock
)- 在操作完成后解锁(
pthread_mutex_unlock
)- 主要改进:
- 检查票数、打印信息、减少票数这个整体过程被互斥锁保护
- 避免了多个线程同时访问
ticket
导致的数据竞争- 确保了一次只有一个线程能进入临界区
- 注意事项:
- 必须在所有可能的退出路径上都要解锁
- 加锁和解锁必须配对
- 锁的粒度要适中(既要保护数据,又不要影响并发性能)
5.3 互斥量实现原理探究
- 经过上面的例子,我们已经意识到单纯的
i++
或者++i
都不是原子的,有可能会有数据一致性问题- 为了实现互斥锁操作,大多数体系结构都提供了
swap
或exchange
指令,该指令的作用是把寄存器和内存单元的数据相交换,由于只有一条指令,保证了原子性,即使是多处理器平台,访问内存的 总线周期也有先后,一个处理器上的交换指令执行时另一个处理器的交换指令只能等待总线周期。 现在我们把lock
和unlock
的伪代码改一下
关于线程与锁的本质交互:
数据交换的本质:
线程要访问数据时,实际是一个"交换"的过程
从内存中的共享数据 → CPU寄存器 → 线程的硬件上下文
锁的交换机制:
锁虽然是共享的,但获取锁的过程是把锁的状态"交换"到线程自己的上下文中
这个交换过程是通过单条汇编指令完成的,确保原子性
相当于线程说:“我要把这个锁标记为我的了”,并且这个标记过程不能被打断
实现特点:
锁的状态变化是通过原子的交换指令完成
交换成功后,这个锁的状态就属于特定线程的上下文了
其他线程看到的是"已被占用"的状态
“锁的本质是让一个线程通过一条不可分割的汇编指令,将共享的锁状态原子地交换到自己的线程上下文中,从而实现对锁的独占访问。”
交换完成后,便是当前线程持有锁了。
5.4 互斥量的封装
Lock.hpp
#pragma once // 防止头文件重复包含
#include <iostream>
#include <string>
#include <pthread.h>namespace LockModule // 锁模块的命名空间
{// Mutex类: 对pthread_mutex_t的面向对象封装class Mutex{public:// 禁用拷贝构造和赋值操作// 因为互斥锁不应该被复制或赋值Mutex(const Mutex &) = delete;const Mutex &operator =(const Mutex &) = delete;// 构造函数:初始化互斥锁Mutex(){int n = pthread_mutex_init(&_mutex, nullptr);(void)n; // 防止编译器警告未使用的变量}// 加锁操作void Lock(){int n = pthread_mutex_lock(&_mutex);(void)n;}// 解锁操作void Unlock(){int n = pthread_mutex_unlock(&_mutex);(void)n;}// 获取原始mutex指针,用于需要原始pthread_mutex_t指针的场景pthread_mutex_t *GetMutexOriginal(){return &_mutex;}// 析构函数:销毁互斥锁~Mutex(){int n = pthread_mutex_destroy(&_mutex);(void)n;}private:pthread_mutex_t _mutex; // 原始的pthread互斥锁};// LockGuard类: RAII风格的锁管理器// RAII(Resource Acquisition Is Initialization)// 利用对象生命周期来管理资源的获取和释放class LockGuard{public:// 构造函数:接收mutex引用并立即加锁LockGuard(Mutex &mutex):_mutex(mutex){_mutex.Lock();}// 析构函数:对象销毁时自动解锁// 保证了即使发生异常,锁也能被正确释放~LockGuard(){_mutex.Unlock();}private:Mutex &_mutex; // 对Mutex对象的引用};
}
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
#include "Lock.hpp" // 包含我们自定义的锁模块using namespace LockModule; // 使用锁模块的命名空间// 共享的全局变量,表示剩余票数
int ticket = 1000;// 全局的互斥锁对象
Mutex mutex;// 售票窗口(线程)的工作函数
void *route(void *arg)
{char *id = (char *)arg; // 转换参数为字符串,表示窗口编号while (1){// 使用RAII风格的锁管理器// 创建LockGuard对象时自动加锁// 当离开作用域时自动解锁LockGuard lockguard(mutex);if (ticket > 0) // 还有票可卖{usleep(1000); // 模拟售票过程耗时printf("%s sells ticket:%d\n", id, ticket);ticket--; // 售出一张票,票数减1}else // 票已售完{break; // 退出循环// LockGuard对象在这里销毁,自动解锁}// 每次循环结束,LockGuard对象销毁,自动解锁}return nullptr;
}int main(void)
{pthread_t t1, t2, t3, t4; // 存储4个线程的线程ID// 创建4个售票线程// 需要将字符串参数强制转换为void*类型pthread_create(&t1, NULL, route, (void*)"thread 1");pthread_create(&t2, NULL, route, (void*)"thread 2");pthread_create(&t3, NULL, route, (void*)"thread 3");pthread_create(&t4, NULL, route, (void*)"thread 4");// 等待所有售票线程结束pthread_join(t1, NULL);pthread_join(t2, NULL);pthread_join(t3, NULL);pthread_join(t4, NULL);// Mutex对象在程序结束时自动销毁
}
6. 可重入VS线程安全
线程安全:多个线程并发同一段代码时,不会出现不同的结果。常见对全局变量或者静态变量进行操作,并且没有锁保护的情况下,会出现该问题。
重入:同一个函数被不同的执行流调用,当前一个流程还没有执行完,就有其他的执行流再次进入,我们称之为重入。一个函数在重入的情况下,运行结果不会出现任何不同或者任何问题,则该函数被称为可重入函数,否则,是不可重入函数。
6.1 常见的线程不安全的情况:
- 不保护共享变量的函数
- 函数状态随着被调用,状态发生变化的函数
- 返回指向静态变量指针的函数
- 调用线程不安全函数的函数
6.2 常见的线程安全的情况
- 每个线程对全局变量或者静态变量只有读取的权限,而没有写入的权限,一般来说这些线程是安全的
- 类或者接口对于线程来说都是原子操作
- 多个线程之间的切换不会导致该接口的执行结果存在二义性
6.3 常见不可重入的情况
- 调用了malloc/free函数,因为malloc函数是用全局链表来管理堆的
- 调用了标准I/O库函数,标准I/O库的很多实现都以不可重入的方式使用全局数据结构
- 可重入函数体内使用了静态的数据结构
6.4 常见可重入的情况
- 不使用全局变量或静态变量
- 不使用用malloc或者new开辟出的空间
- 不调用不可重入函数
- 不返回静态或全局数据,所有数据都有函数的调用者提供
- 使用本地数据,或者通过制作全局数据的本地拷贝来保护全局数据
6.5 可重入与线程安全联系
- 函数是可重入的,那就是线程安全的
- 函数是不可重入的,那就不能由多个线程使用,有可能引发线程安全问题
- 如果一个函数中有全局变量,那么这个函数既不是线程安全也不是可重入的。
6.6 可重入与线程安全区别
- 可重入函数是线程安全函数的一种
- 线程安全不一定是可重入的,而可重入函数则一定是线程安全的。
- 如果将对临界资源的访问加上锁,则这个函数是线程安全的,但如果这个重入函数若锁还未释放则会产生死锁,因此是不可重入的。
7. 常见锁概念
7.1 死锁
- 死锁是指在一组进程中的各个进程均占有不会释放的资源,但因互相申请被其他进程所站用不会释放的资源而处于的一种永久等待状态。
- 为了方便表述,假设现在线程A,线程B必须同时持有锁1和锁2,才能进行后续资源的访问
申请一把锁是原子的,但是申请两把锁就不一定了
造成的结果是
7.2 死锁的四个必要条件
- 互斥条件:一个资源每次只能被一个执行流使用
- 请求与保持条件:一个执行流因请求资源而阻塞时,对已获得的资源保持不放
不剥夺条件:一个执行流已获得的资源,在末使用完之前,不能强行剥夺
循环等待条件:若干执行流之间形成一种头尾相接的循环等待资源的关系
7.3 避免死锁
破坏死锁的四个必要条件:(只需要一个不满足就好了)
- 互斥条件:一个资源每次只能被一个执行流使用
- 请求与保持条件:一个执行流因请求资源而阻塞时,对已获得的资源保持不放
- 不剥夺条件:一个执行流已获得的资源,在末使用完之前,不能强行剥夺
- 循环等待条件:若干执行流之间形成一种头尾相接的循环等待资源的关系
破坏循环等待条件问题:资源一次性分配, 使用超时机制、加锁顺序一致、避免锁未释放的场景
#include <iostream>
#include <mutex> // 用于互斥锁
#include <thread> // 用于创建和管理线程
#include <vector> // 用于存储线程对象
#include <unistd.h> // Unix标准头文件,提供sleep等系统调用// 定义两个全局共享资源(整数变量)
int shared_resource1 = 0;
int shared_resource2 = 0;
// 定义两个互斥锁用于保护共享资源
std::mutex mtx1, mtx2;// 函数:同时访问两个共享资源
void access_shared_resources()
{// 以下是被注释掉的正确加锁方式// 创建两个unique_lock对象,但初始时不加锁// std::unique_lock<std::mutex> lock1(mtx1, std::defer_lock);// std::unique_lock<std::mutex> lock2(mtx2, std::defer_lock);// 使用std::lock同时锁定两个互斥锁// std::lock可以避免死锁,它要么锁定所有互斥量,要么一个都不锁定// std::lock(lock1, lock2);// 模拟大量操作int cnt = 10000;while (cnt){// 没有锁保护的情况下,这里会发生数据竞争++shared_resource1; // 竞争点1++shared_resource2; // 竞争点2cnt--;}// 被注释代码的析构说明:// 当函数结束时,lock1和lock2会被析构// unique_lock的析构函数会自动解锁互斥量// 这就是RAII(资源获取即初始化)的应用
}// 函数:模拟多线程并发访问场景
void simulate_concurrent_access()
{// 创建线程容器std::vector<std::thread> threads;// 创建10个线程来模拟并发访问for (int i = 0; i < 10; ++i){// emplace_back会直接在vector中构造线程对象// 每个线程都执行access_shared_resources函数threads.emplace_back(access_shared_resources);}// 等待所有线程完成// 必须调用join,否则会导致程序异常终止for (auto &thread : threads){thread.join();}// 输出最终结果// 由于没有加锁保护,结果可能小于预期值(10 * 10000)std::cout << "Shared Resource 1: " << shared_resource1 << std::endl;std::cout << "Shared Resource 2: " << shared_resource2 << std::endl;
}int main()
{simulate_concurrent_access();return 0;
}
运行结果:
// 不一次申请
Shared Resource 1: 94416
Shared Resource 2: 94536
// 一次申请
Shared Resource 1: 100000
Shared Resource 2: 100000
- 没有加锁保护时的问题(当前运行代码):
while (cnt)
{++shared_resource1; // 竞争点1++shared_resource2; // 竞争点2cnt--;
}
- 多个线程同时访问和修改shared_resource1和shared_resource2
- 导致了数据竞争(race condition)
- 结果是94416和94536,小于预期的100000
- 这是因为某些++操作被覆盖或丢失了
- 正确的加锁方式(被注释的代码):
std::unique_lock<std::mutex> lock1(mtx1, std::defer_lock);
std::unique_lock<std::mutex> lock2(mtx2, std::defer_lock);
std::lock(lock1, lock2);
特点:
- 使用
std::defer_lock
创建但不立即锁定- 使用
std::lock
同时获取两个锁- 避免了死锁问题
- 保证了结果正确(100000)
- 为什么要同时获取锁?
// 错误示范 - 可能死锁
mtx1.lock();
mtx2.lock();
如果分别获取锁,可能导致死锁:
线程A:先锁mtx1,等待mtx2
线程B:先锁mtx2,等待mtx1
导致互相等待,形成死锁
std::lock
的优势:
要么全部锁定成功
要么一个都不锁定
使用固定顺序获取锁
完全避免死锁风险
RAII(资源获取即初始化)的应用:
unique_lock
在析构时自动释放锁即使发生异常,也能确保锁被释放
不需要手动调用unlock()
7.4 避免死锁算法
- 死锁检测算法(了解)
- 银行家算法(了解)
8. 线程同步
8.1 条件变量
- 当一个线程互斥地访问某个变量时,它可能发现在其它线程改变状态之前,它什么也做不了。
- 例如一个线程访问队列时,发现队列为空,它只能等待,只到其它线程将一个节点添加到队列中。这种情况就需要用到条件变量。
8.2 同步概念与竞态条件
- 同步:在保证数据安全的前提下,让线程能够按照某种特定的顺序访问临界资源,从而有效避免饥饿问题,叫做同步
- 竞态条件:因为时序问题,而导致程序异常,我们称之为竞态条件。在线程场景下,这种问题也不难理解
8.3 条件变量函数
初始化:
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)
返回值(int):0:成功非0:失败,返回错误码
参数(pthread_cond_t *cond):pthread_cond_t: 条件变量类型*cond: 指向条件变量的指针传入指针允许函数修改条件变量的状态
等待条件满足
int pthread_cond_wait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex);
参数:cond:要在这个条件变量上等待mutex:互斥量,后面详细解释
唤醒等待
int pthread_cond_signal(pthread_cond_t *cond); // 唤醒一个等待线程
int pthread_cond_broadcast(pthread_cond_t *cond); // 唤醒所有等待线程
返回值:return 0; // 成功return EINVAL; // 失败:无效的条件变量指针
简单案例:
- 我们先使用
PTHREAD_COND
/MUTEX_INITIALIZER
进行测试,对其他细节暂不追究- 然后将接口更改成为使用
pthread_cond_init
/pthread_cond_destroy
的方式,方便后续进行封装
#include <iostream>
#include <string.h>
#include <unistd.h>
#include <pthread.h>// 全局条件变量,静态初始化
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
// 与条件变量配套使用的互斥锁,静态初始化
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;// 线程执行函数
void *active( void *arg )
{// 将void*参数转换为字符串,作为线程名称std::string name = static_cast<const char*>(arg);while (true){// 加锁,保护条件变量pthread_mutex_lock(&mutex);// pthread_cond_wait做了三件事:// 1. 释放mutex// 2. 阻塞等待条件变量信号// 3. 当被唤醒时,重新获取mutexpthread_cond_wait(&cond, &mutex);// 被唤醒后执行的代码std::cout << name << " 活动..." << std::endl;// 解锁pthread_mutex_unlock(&mutex);}
}int main( void )
{pthread_t t1, t2; // 定义两个线程ID// 创建两个线程,传入不同的线程名称pthread_create(&t1, NULL, active, (void*)"thread-1");pthread_create(&t2, NULL, active, (void*)"thread-2");// 主线程睡眠3秒// 确保两个子线程已经运行并进入等待状态sleep(3);while(true){// 两种唤醒方式:// pthread_cond_signal(&cond); // 唤醒一个等待的线程pthread_cond_broadcast(&cond); // 唤醒所有等待的线程sleep(1); // 每秒钟唤醒一次}// 等待两个线程结束(实际上在这个程序中不会执行到这里)pthread_join(t1, NULL);pthread_join(t2, NULL);
}
运行结果:
thread-1 活动...
thread-2 活动...
thread-1 活动...
thread-1 活动...
thread-2 活动...
这是一个条件变量的示例程序:
- 条件变量的作用:
- 用于线程之间的通知机制
- 允许线程等待某个条件成立
- 当条件满足时,可以唤醒等待的线程
- 主要组件:
pthread_cond_t
: 条件变量pthread_mutex_t
: 配套的互斥锁pthread_cond_wait
: 等待条件pthread_cond_signal
: 唤醒一个线程pthread_cond_broadcast
: 唤醒所有线程- 程序流程:
- 两个线程创建后进入循环等待状态
- 主线程每隔1秒唤醒线程
- 被唤醒的线程输出信息后继续等待
signal vs broadcast
:
signal
只唤醒一个线程broadcast
唤醒所有等待的线程- 在这个例子中可以对比测试两种效果
- 注意事项:
- 条件变量必须配合互斥锁使用
wait
操作会自动处理互斥锁的释放和重新获取- 使用条件变量时要注意虚假唤醒问题