OS / Linux / pthread_cond_wait 为什么需要传递 mutex 参数?

这篇博客详细解释了互斥锁在保护条件变量外部判断条件中的作用,以及为什么pthread_cond_wait需要传入互斥锁。通过分析线程A的wait过程和线程B的修改条件及唤醒操作,阐述了锁的必要性和使用技巧,强调了防止虚假唤醒的重要性。

栗子:

mutex.lock();

while (判断“条件”是否成立) {
    pthread_cond_wait 等待
}

mutext.unlock();

条件变量这个互斥锁,不是用来保护条件变量的内部状态。而是用来保护外部“条件”(就是那个 while 循环中的判断)。假如条件变量的内部状态需要锁定,完全可以在内部实现中维护一个锁,没有必要从外部传进来。

用于判断是否等待的“条件”,通常就是线程之间共享的某个变量值。A 线程判断“条件”等待,会读取共享变量。B 线程让“条件”满足,会修改变量。这个锁,就是保护线程间的共享变量(“条件”),让“条件”不会一边修改,一边读取。

这把锁还有个额外的作用。wait 函数会阻塞,将线程 A 放到阻塞队列中。wait 函数放在锁的 lock,unlock 内部,也保证了线程还没有真正放到阻塞队列时,线程 B 不能修改“条件”后进行唤醒。这段话应该暂时读不懂,可回头再看,先放在这里,不然描述就不够准确。

不同的应用会有不同的“条件”,在实现条件变量这个库时,是没有办法预先知道这个“条件”究竟是什么。因此要保护“条件”,就只能让用户自己在库外部分配锁。

弄清了这个互斥锁究竟在保护什么。这时我们才能进一步讨论 pthread_cond_wait 为什么要传入这个锁呢?这点就跟 wait 的实现有关。

先来看等待线程 A 的写法。

mutex.lock();

while (判断“条件”是否成立) {
    pthread_cond_wait 等待
}

mutext.unlock();

判断“条件”是否成立,在 lock, unlock 之间受到保护。pthread_cond_wait 可能会阻塞,假如真的阻塞,mutex 这锁就一直不能被释放了。因此在 pthread_cond_wait 的实现内部,在阻塞之前,必须先将锁释放。在唤醒的时候,再重新获取锁。

pthread_cond_wait 展开,内部实现中,会有下面的过程。

做一些其它事情
mutext.unlock()
阻塞中...
唤醒
mutext.lock()

假如在线程 A 中完全展开 pthread_cond_wait

mutex.lock();

while (判断“条件”是否成立) {
    做一些其它事情
    mutext.unlock()
    阻塞
    唤醒
    mutext.lock()
}

mutext.unlock();

可以看到,判断“条件” 一直在 mutext 的 lock, unlock 之间,受到保护。

来到这里,我们总结一下。

  1. 跟条件变量关联的互斥锁,是用于保护判断“条件”,并非条件变量的内部状态。
  2. 将互斥锁,传入 wait 的实现中,是让条件变量库可以在适当的时机释放(unlock)和重新获取(lock)这把锁。

注意上面第 2 点中,“适当时机”这个词。假如不将这个锁传入 pthread_cond_wait,让用户自己在阻塞前先释放锁,是没有办法做到适当时机的。阻塞队列在条件变量库内部维护,只有条件变量库内部才能控制这个时机。

wait 会阻塞,将线程 A 放到阻塞队列中。在线程还没有真正放到阻塞队列时,需要一直上锁。不然另一线程 B 修改“条件”后进行唤醒,这个线程 A 还没有在阻塞队列中,就不能被唤醒了。因而这把保护外部判断“条件”的锁,需要传入到 wait 函数中。等将线程真正放到阻塞队列后才能解锁,之后线程被唤醒后再重新获取。

线程 A 的 pthread_cond_wait 需要传入 mutext,并且 pthread_cond_wait 一定要在 lock 和 unlock 之间。

至于线程 B, 用于修改“条件”。修改“条件” 也必须在 lock 和 unlock 之间,受互斥锁保护。至于 pthread_cond_signal 是否在 lock 和 unlock 之间,其实是没有关系的。但 pthread_cond_signal 必须在修改“条件”之后,不然可能 A 线程被唤醒后,条件不满足继续等待,这个唤醒信号就丢失了。

mutext.lock()
修改”条件“
pthread_cond_signal 唤醒
mutext.unlock()

也可以将 pthread_cond_signal 移到 unlock 外部。

mutext.lock()
修改”条件“
mutext.unlock()
pthread_cond_signal 唤醒

条件变量等待时,那个 wait 的判断,需要使用 while 循环,而不是 if,是防止虚假唤醒。就不再一一分析了。

 

作者:黄兢成
链接:https://www.zhihu.com/question/24116967/answer/676751734
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

 

(SAW:Game Over!)

<think>我们正在讨论C++中的多线程同步机制,具体是关于`pthread_cond_t`(条件变量)和`pthread_mutex_t`(互斥锁)的作用及使用方法。根据引用内容,我们可以总结以下几点: 1. 互斥锁(`pthread_mutex_t`)用于保护临界资源,确保同一时间只有一个线程可以访问临界区。 2. 条件变量(`pthread_cond_t`)用于线程间的同步,允许线程在条件不满足时挂起等待,并在条件满足时被唤醒。 3. 使用条件变量时,通常与互斥锁配合使用,以防止竞争条件。 引用中特别提到: - `pthread_cond_signal`用于唤醒一个等待在条件变量上的线程(引用[1]和[2])。 - `pthread_cond_broadcast`用于唤醒所有等待在条件变量上的线程(引用[3])。 - 条件变量的使用模式:在调用`pthread_cond_wait`前必须持有互斥锁,该函数会释放锁并进入等待,被唤醒时会重新获得锁(引用[4])。 现在,我们详细说明这两个类型的作用及使用方法。 ### 一、互斥锁(`pthread_mutex_t`)的作用 互斥锁用于保护临界区,确保多个线程不会同时访问共享资源,从而避免数据不一致的问题。其基本操作包括: - 初始化:`pthread_mutex_init()` - 加锁:`pthread_mutex_lock()` - 解锁:`pthread_mutex_unlock()` - 销毁:`pthread_mutex_destroy()` ### 二、条件变量(`pthread_cond_t`)的作用 条件变量用于线程间的条件等待。当某个条件不满足时,线程可以等待在条件变量上;当另一个线程改变了条件并发出信号时,等待的线程可以被唤醒。条件变量总是与互斥锁一起使用。基本操作包括: - 初始化:`pthread_cond_init()` - 等待:`pthread_cond_wait()` - 唤醒一个等待线程:`pthread_cond_signal()` - 唤醒所有等待线程:`pthread_cond_broadcast()` - 销毁:`pthread_cond_destroy()` ### 三、使用方法 #### 1. 初始化 ```cpp pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; pthread_cond_t cond = PTHREAD_COND_INITIALIZER; ``` 或者使用动态初始化: ```cpp pthread_mutex_t mutex; pthread_cond_t cond; pthread_mutex_init(&mutex, NULL); pthread_cond_init(&cond, NULL); ``` #### 2. 等待条件 典型的等待条件模式如下: ```cpp pthread_mutex_lock(&mutex); while (condition_is_false) { pthread_cond_wait(&cond, &mutex); } // 条件满足后执行操作 pthread_mutex_unlock(&mutex); ``` 注意:`pthread_cond_wait`会原子地释放互斥锁并进入等待状态,当被唤醒时,它会重新获得互斥锁。 #### 3. 发送条件信号 改变条件并通知等待线程: ```cpp pthread_mutex_lock(&mutex); // 修改条件 condition_is_false = false; // 唤醒一个等待线程 pthread_cond_signal(&cond); // 或者唤醒所有等待线程 // pthread_cond_broadcast(&cond); pthread_mutex_unlock(&mutex); ``` ### 四、注意事项 1. **条件检查**:在调用`pthread_cond_wait`前必须检查条件,并且通常使用`while`循环而不是`if`语句,以避免虚假唤醒(spurious wakeups)[^4]。 2. **锁的保护**:修改条件变量相关的条件时,必须在互斥锁的保护下进行。 3. **信号的位置**:`pthread_cond_signal`可以放在互斥锁锁定和解锁之间(效率较高)或之后(避免唤醒的线程立即阻塞,但效率稍低)。引用[1]提到两种放置方式各有缺点,通常放在锁内(解锁之前)更常见。 ### 五、示例代码 以下是一个简单的生产者-消费者模型,其中使用条件变量和互斥锁实现同步: ```cpp #include <pthread.h> #include <iostream> #include <queue> std::queue<int> queue; pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; pthread_cond_t cond = PTHREAD_COND_INITIALIZER; void* producer(void* arg) { for (int i = 0; i < 10; i++) { pthread_mutex_lock(&mutex); queue.push(i); std::cout << "Produced: " << i << std::endl; pthread_cond_signal(&cond); // 发送信号 pthread_mutex_unlock(&mutex); } return NULL; } void* consumer(void* arg) { while (true) { pthread_mutex_lock(&mutex); while (queue.empty()) { pthread_cond_wait(&cond, &mutex); // 等待条件 } int value = queue.front(); queue.pop(); std::cout << "Consumed: " << value << std::endl; pthread_mutex_unlock(&mutex); if (value == 9) break; // 退出条件 } return NULL; } int main() { pthread_t producer_thread, consumer_thread; pthread_create(&producer_thread, NULL, producer, NULL); pthread_create(&consumer_thread, NULL, consumer, NULL); pthread_join(producer_thread, NULL); pthread_join(consumer_thread, NULL); return 0; } ``` ### 六、死锁与资源管理 - 确保在不再使用锁和条件变量时销毁它们。 - 避免在持有锁时长时间阻塞或进行耗时操作。 §§相关问题§§ 1. 如何避免使用条件变量时出现死锁? 2. 什么是虚假唤醒(spurious wakeup)?如何防止? 3. 在多线程编程中,除了互斥锁和条件变量,还有哪些同步机制? </think>### C++ 中 `pthread_cond_t` 和 `pthread_mutex_t` 的作用与使用方法 `pthread_cond_t`(条件变量)和 `pthread_mutex_t`(互斥锁)是 POSIX 线程库的核心同步原语,用于解决多线程编程中的**共享资源竞争**和**线程协作**问题。它们联合使用可实现高效的线程同步机制。 --- ### 一、核心作用对比 | **类型** | **作用** | **使用场景** | |--------------------|--------------------------------------------------------------------------|--------------------------------| | `pthread_mutex_t` | 保护临界资源,确保同一时间只有 1 个线程访问共享数据 | 防止数据竞争 | | `pthread_cond_t` | 实现线程间通信,允许线程在条件不满足时挂起,条件满足时被唤醒 | 生产者-消费者模型、任务调度等 | --- ### 二、声明与初始化 #### 1. 静态初始化(推荐) ```cpp pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; pthread_cond_t cond = PTHREAD_COND_INITIALIZER; ``` #### 2. 动态初始化 ```cpp pthread_mutex_t mutex; pthread_cond_t cond; pthread_mutex_init(&mutex, NULL); // 第二个参数为属性,NULL 表示默认 pthread_cond_init(&cond, NULL); ``` --- ### 三、核心使用模式 #### 1. **等待线程**的标准流程 ```cpp pthread_mutex_lock(&mutex); // ① 进入临界区前加锁 while (condition_is_false) { // ② 必须用 while 循环检查条件 pthread_cond_wait(&cond, &mutex); // ③ 原子操作:解锁 + 等待 } // 条件满足时执行操作... pthread_mutex_unlock(&mutex); // ④ 操作完成后解锁 ``` #### 2. **通知线程**的标准流程 ```cpp pthread_mutex_lock(&mutex); // ① 修改条件前加锁 make_condition_true(); // ② 修改共享状态 pthread_cond_signal(&cond); // ③ 发送信号(唤醒一个等待线程) // 或 pthread_cond_broadcast(&cond); // 唤醒所有等待线程 pthread_mutex_unlock(&mutex); // ④ 解锁 ``` --- ### 四、关键函数解析 1. **`pthread_cond_wait`** - **原子操作**:同时释放锁并进入等待状态,唤醒时自动重新获取锁 - **必须与互斥锁配合使用**:防止唤醒丢失(wakeup loss) 2. **`pthread_cond_signal`** - 唤醒**至少一个**等待该条件变量的线程 - 通常放在临界区内(锁保护下)以避免竞争[^1] 3. **`pthread_cond_broadcast`** - 唤醒**所有**等待该条件变量的线程[^3] - 适用场景:多个线程可并行处理同一事件 --- ### 五、典型应用示例(生产者-消费者模型) ```cpp std::queue<int> buffer; // 共享缓冲区 pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; pthread_cond_t cond = PTHREAD_COND_INITIALIZER; // 生产者线程 void* producer(void*) { pthread_mutex_lock(&mutex); buffer.push(42); // 生产数据 pthread_cond_signal(&cond); // 通知消费者 pthread_mutex_unlock(&mutex); return nullptr; } // 消费者线程 void* consumer(void*) { pthread_mutex_lock(&mutex); while (buffer.empty()) { // 必须循环检查 pthread_cond_wait(&cond, &mutex); // 等待通知 } int data = buffer.front(); buffer.pop(); // 消费数据 pthread_mutex_unlock(&mutex); return nullptr; } ``` --- ### 六、关键注意事项 1. **虚假唤醒(Spurious Wakeup)** 条件变量可能无故唤醒线程,因此必须用 `while` 而非 `if` 检查条件[^4]: $$ \text{正确: } while\text{(condition)}\ \text{pthread\_cond\_wait(...)} $$ 2. **信号位置争议** `pthread_cond_signal` 放在锁内或锁外各有优劣[^1]: - **锁内调用**(更安全):避免信号丢失 - **锁外调用**(更高性能):减少上下文切换 3. **资源销毁** ```cpp pthread_mutex_destroy(&mutex); pthread_cond_destroy(&cond); ``` --- ### 七、与 C++ 标准库对比 | **POSIX** | **C++11 等效** | |-----------------------|------------------------| | `pthread_mutex_t` | `std::mutex` | | `pthread_cond_t` | `std::condition_variable` | | `pthread_cond_wait` | `cv.wait(lock, predicate)` | | `pthread_cond_signal` | `cv.notify_one()` | > 💡 优先使用 C++ 标准库(如 `std::condition_variable`),除非需兼容旧系统或跨平台。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值