5. Pthread 条件变量
条件变量提供了一种线程之间的同步方式。通过互斥锁来控制线程对数据的访问,而条件变量则允许线程根据数据的实际值来同步。
条件变量的典型用法是:
- 当一个线程对数据进行修改后,其他等待数据达到特定状态的线程会被唤醒。
- 条件变量总是与互斥锁一起使用,以确保共享数据的访问安全。
5.1. Pthread 条件变量的声明
条件变量可以通过 pthread_cond_t
类型声明。
示例声明:
pthread_cond_t count_threshold_cv; // 声明一个条件变量
5.2. Pthread 条件变量的初始化
条件变量在使用之前需要进行初始化,可以使用 pthread_cond_init()
函数。
int pthread_cond_init(pthread_cond_t *condition, const pthread_condattr_t *attr);
- 参数:
condition
:指向要初始化的条件变量的指针。attr
:条件变量的属性,如果为NULL
,则使用默认属性。
示例初始化:
pthread_cond_init(&count_threshold_cv, NULL); // 使用默认属性初始化条件变量
5.3. Pthread 条件变量的销毁
在条件变量不再使用时,应将其销毁以释放系统资源。
int pthread_cond_destroy(pthread_cond_t *condition);
示例销毁:
pthread_cond_destroy(&count_threshold_cv); // 销毁条件变量
5.4. Pthread 条件变量的操作函数
-
pthread_cond_wait(pthread_cond_t *condition, pthread_mutex_t *mutex)
:- 这个函数使调用线程进入等待状态,直到指定的条件变量被信号唤醒。此函数应该在互斥锁被锁住时调用,它会自动释放互斥锁并等待。
-
pthread_cond_signal(pthread_cond_t *condition)
:- 唤醒一个等待在该条件变量上的线程。应在持有互斥锁的情况下调用。
-
pthread_cond_broadcast(pthread_cond_t *condition)
:- 唤醒所有等待在该条件变量上的线程。
5.5 示例代码:使用条件变量实现线程同步
下面的例子展示了如何使用条件变量和互斥锁来实现多个线程的同步操作。
示例代码
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#define NUM_THREADS 3
#define TCOUNT 10
#define COUNT_LIMIT 10
int count = 0; // 共享变量
int thread_ids[3] = {0, 1, 2};
pthread_mutex_t count_mutex; // 互斥锁
pthread_cond_t count_threshold_cv; // 条件变量
// 线程函数:计数增加
void *inc_count(void *idp) {
int i = 0;
int *my_id = (int *)idp;
for (i = 0; i < TCOUNT; i++) {
pthread_mutex_lock(&count_mutex); // 锁定互斥锁
count++;
if (count == COUNT_LIMIT) {
// 当 count 达到 COUNT_LIMIT 时,发出条件信号
pthread_cond_signal(&count_threshold_cv);
}
printf("inc_count(): thread %d, count = %d, unlocking mutex\n", *my_id, count);
pthread_mutex_unlock(&count_mutex); // 解锁互斥锁
sleep(1);
}
printf("inc_count(): thread %d, Threshold reached.\n", *my_id);
pthread_exit(NULL);
}
// 线程函数:监视计数
void *watch_count(void *idp) {
int *my_id = (int *)idp;
printf("Starting watch_count(): thread %d\n", *my_id);
pthread_mutex_lock(&count_mutex); // 锁定互斥锁
while (count < COUNT_LIMIT) {
// 等待条件信号,当条件满足时自动锁定互斥锁
pthread_cond_wait(&count_threshold_cv, &count_mutex);
printf("watch_count(): thread %d Condition signal received.\n", *my_id);
}
// 当条件满足后进行的操作
count += 100;
pthread_mutex_unlock(&count_mutex); // 解锁互斥锁
pthread_exit(NULL);
}
int main(int argc, char *argv[]) {
int i, rc;
pthread_t threads[3];
pthread_attr_t attr;
// 初始化互斥锁和条件变量
pthread_mutex_init(&count_mutex, NULL);
pthread_cond_init(&count_threshold_cv, NULL);
// 显式创建可连接的线程
pthread_attr_init(&attr);
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_JOINABLE);
// 创建线程
pthread_create(&threads[0], &attr, watch_count, (void *)&thread_ids[0]);
pthread_create(&threads[1], &attr, inc_count, (void *)&thread_ids[1]);
pthread_create(&threads[2], &attr, inc_count, (void *)&thread_ids[2]);
// 等待所有线程完成
for (i = 0; i < NUM_THREADS; i++) {
pthread_join(threads[i], NULL);
}
// 清理资源
printf("Main(): Waited on %d threads. Done.\n", NUM_THREADS);
pthread_attr_destroy(&attr);
pthread_mutex_destroy(&count_mutex);
pthread_cond_destroy(&count_threshold_cv);
pthread_exit(NULL);
return 0;
}
代码解释
-
共享变量
count
和 互斥锁count_mutex
:count
是所有线程共享的变量,需要通过互斥锁count_mutex
来保护。
-
线程函数
inc_count()
:- 每个线程循环增加
count
,并在count
达到COUNT_LIMIT
时,调用pthread_cond_signal()
向条件变量发出信号。 - 通过
pthread_mutex_lock()
和pthread_mutex_unlock()
来锁定和解锁共享变量,确保线程安全。
- 每个线程循环增加
-
线程函数
watch_count()
:- 等待
count
达到COUNT_LIMIT
,调用pthread_cond_wait()
进行等待。当条件满足时,接收到信号,并进行进一步操作。 pthread_cond_wait()
会在等待期间释放互斥锁,并在被唤醒时重新获取锁。
- 等待
-
主函数
main()
:- 创建 3 个线程,其中一个是监视线程,两个是计数线程。
- 使用
pthread_join()
等待所有线程完成任务。 - 最后清理所有的资源,包括互斥锁和条件变量。
输出分析
Starting watch_count(): thread 0
inc_count(): thread 1, count = 1, unlocking mutex
inc_count(): thread 2, count = 2, unlocking mutex
...
inc_count(): thread 2, count = 10, unlocking mutex
watch_count(): thread 0 Condition signal received.
inc_count(): thread 1, count = 111, unlocking mutex
...
- 线程
inc_count()
增加count
,并在达到COUNT_LIMIT
时通过pthread_cond_signal()
发出信号。 - 线程
watch_count()
在条件满足时被唤醒,进行相应的操作。
通俗解释
条件变量就像一个警报器,当某个特定条件满足时,系统就会通知等待的线程。例如,线程在增加一个计数器的值,当这个值达到某个阈值时,会通知其他线程“条件达到了”,等待这个条件的线程就会被唤醒并继续执行任务。
在这个例子中,当计数值 count
达到 COUNT_LIMIT
,监视线程 watch_count()
就会被唤醒,然后对计数进行进一步处理。
总结
- 条件变量用于线程间的同步,允许线程根据数据的值来进行操作。
pthread_cond_wait()
使线程等待某个条件的满足。pthread_cond_signal()
和pthread_cond_broadcast()
用于通知等待中的线程。- 条件变量需要与互斥锁结合使用,以确保数据的安全访问。
条件变量和互斥锁需要协调使用是为了在多线程环境下实现安全、有效的数据同步。这种协调的必要性可以从两个主要方面来理解:互斥保护和条件等待。下面我会详细说明条件变量和互斥锁如何配合,及其背后的原因。
1. 互斥保护数据一致性
在多线程编程中,共享资源可能同时被多个线程访问。为了保证数据的正确性,需要使用互斥锁来控制线程对共享数据的访问。互斥锁(mutex
)的作用是确保在任意时刻只有一个线程可以访问共享资源,防止数据竞争情况的发生。
举个例子,如果有多个线程试图同时对一个变量进行加减操作,而不使用互斥锁,那么这个变量的值可能会变得不可预测。例如两个线程都读取了相同的变量值然后进行计算,结果覆盖时会互相冲突,从而导致数据的结果错误。
2. 条件变量实现线程同步
条件变量(condition variable
)的作用是允许线程在某种特定的条件下进行等待,并在该条件被满足时被唤醒。这为线程之间提供了一种同步的机制,即线程之间可以基于特定条件的改变来协调它们的行为。
-
为什么需要互斥锁配合条件变量?
-
防止竞争条件:条件变量通常用来等待某个特定条件的达成,例如等待某个计数器达到某个值。如果我们在不加锁的情况下对这个条件进行检查,就可能发生竞争条件。例如,一个线程可能在检查条件时被另一个线程打断,而后者改变了条件的状态,从而使原先的条件检查变得无效。
-
保护共享状态的一致性:条件变量与互斥锁一起使用,是为了在修改共享状态以及等待该状态变化时,保证一致性。每当线程要等待某个条件时,必须首先锁住互斥锁,确保没有其他线程在修改该状态。
-
条件变量需要互斥锁的保护:
pthread_cond_wait()
函数会自动释放锁并进入等待状态。当条件满足时,它会重新锁定互斥锁并继续执行。这样可以保证在条件变量释放锁期间,其他线程仍然可以安全地访问或修改共享数据。
-
3. 条件变量与互斥锁如何协调?
-
获取互斥锁:在使用条件变量前,线程首先要获取互斥锁(
pthread_mutex_lock()
)。这一步是为了确保共享数据的状态不会被其他线程同时修改。 -
检查条件:在锁住互斥锁后,线程检查共享数据是否满足某个条件。通常这部分代码会放在
while
循环中,以确保线程被唤醒后条件依然有效。这是因为在被唤醒后,其他线程可能已经修改了共享数据。 -
等待条件:如果条件不满足,线程调用
pthread_cond_wait()
函数。在调用该函数时,互斥锁会被释放,这样其他线程可以继续执行并修改共享数据。当条件满足时,线程被唤醒并重新锁住互斥锁。 -
处理共享数据:当线程被唤醒后,再次锁住互斥锁,并继续操作共享数据。
-
解锁互斥锁:在操作完成后,线程释放互斥锁(
pthread_mutex_unlock()
)。
4. 举个例子
假设有一个线程负责生产数据,另一个线程负责消费数据,这里就可以使用条件变量和互斥锁进行同步。
- 生产者线程会填充缓冲区,并在数据准备好之后,通过条件变量通知消费者线程可以读取数据。
- 消费者线程会等待条件变量,直到生产者线程通知它数据已准备好。为了防止在数据未准备好时进行读取,消费者线程必须先锁住互斥锁,然后等待条件变量。
协调过程如下:
-
生产者线程:
- 获取互斥锁。
- 修改共享数据(例如向缓冲区写入数据)。
- 发出条件信号(
pthread_cond_signal()
),通知等待的线程。 - 释放互斥锁。
-
消费者线程:
- 获取互斥锁。
- 检查缓冲区状态,如果没有数据,则调用
pthread_cond_wait()
,并自动释放互斥锁。 - 当接收到生产者的信号时,
pthread_cond_wait()
返回,消费者线程重新锁定互斥锁。 - 消费数据,并释放互斥锁。
5. 为什么不直接使用互斥锁来同步?
虽然互斥锁可以用来防止共享数据的竞争访问,但它只能确保数据在某个时刻只能被一个线程访问。互斥锁本身并不能实现“等待某个条件成立”的功能,而条件变量则可以帮助实现这种功能。
通过条件变量,线程可以进入等待状态,直到某个特定条件(比如数据已经就绪)被满足。这种方式比不断轮询(即反复检查条件是否满足)更高效,因为轮询会消耗大量的 CPU 资源。而条件变量让线程在条件不满足时“休眠”,从而释放系统资源,等待被唤醒。