一、基础概念
条件变量(Condition Variables)是并发编程中用于线程同步的一种机制,它通常与互斥锁(Mutex)一起使用,以允许线程以某种条件为基础来挂起(阻塞)和唤醒(继续执行)。条件变量提供了两种基本操作:等待(wait)和通知(notify/notifyAll)。
二、注意事项
-
条件变量必须和互斥锁一起使用,以避免竞态条件。
-
在调用
pthread_cond_wait
之前,线程必须持有与条件变量关联的互斥锁。 -
调用
pthread_cond_wait
时,线程会释放互斥锁并进入等待状态,直到被唤醒并重新获得互斥锁。 -
在调用
pthread_cond_signal
或pthread_cond_broadcast
之前,线程也必须持有与条件变量关联的互斥锁。 -
使用条件变量时,通常使用
while
循环来检查条件是否满足,而不是if
语句,以避免虚假唤醒。
三、常用函数【POSIX线程(pthread)库】
3.1 初始化条件变量
int pthread_cond_init(pthread_cond_t *cond, const pthread_condattr_t *attr);
参数说明:
-
cond: 指向条件变量的指针。
-
attr: 指向条件变量属性的指针,通常设置为NULL,表示使用默认属性。
-
返回值: 成功返回0,失败返回错误码。
3.2 销毁条件变量
int pthread_cond_destroy(pthread_cond_t *cond);
参数说明:
-
cond: 指向条件变量的指针。
-
返回值: 成功返回0,失败返回错误码。
3.3 pthread_cond_wait
int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);
将当前线程挂起,直到有信号将其唤醒或出现错误返回。在等待期间,会释放与条件变量关联的互斥锁,当线程被唤醒后重新获取互斥锁。
参数说明:
-
cond: 指向条件变量的指针。
-
mutex: 指向互斥锁的指针。
-
返回值: 成功返回0,失败返回错误码。
3.4 pthread_cond_timedwait
int pthread_cond_timedwait(pthread_cond_t *cond, pthread_mutex_t *mutex, const struct timespec *abstime);
类似于pthread_cond_wait
,但加入了超时限制。如果在超时时间之前条件没有满足,函数将返回一个错误码。
参数说明:
-
cond: 指向条件变量的指针。
-
mutex: 指向互斥锁的指针。
-
abstime:指向
timespec
结构体的指针,表示等待的绝对时间。 -
返回值: 成功返回0,超时返回
ETIMEDOUT
,其他失败返回错误码。
3.5 pthread_cond_signal
int pthread_cond_signal(pthread_cond_t *cond);
唤醒等待给定条件变量的一个线程。如果有多个线程在等待,则唤醒其中一个线程。
参数说明:
-
cond: 指向条件变量的指针。
-
返回值: 成功返回0,失败返回错误码。
3.6 pthread_cond_broadcast
int pthread_cond_broadcast(pthread_cond_t *cond);
唤醒所有等待给定条件变量的线程。
参数说明:
-
cond: 指向条件变量的指针。
-
返回值: 成功返回0,失败返回错误码。
四、代码示例
4.1 示例1
以下是一个简单的示例,展示了如何使用条件变量和互斥锁来实现生产者-消费者模型。
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
int buffer = 0; // 假设这是一个简单的缓冲区,用于存储整数
void* producer(void* arg) {
for (int i = 0; i < 10; ++i) {
pthread_mutex_lock(&mutex);
// 生产者生产产品,这里简单地将buffer加1
buffer++;
printf("Producer produced %d\n", buffer);
// 唤醒可能在等待的消费者
pthread_cond_signal(&cond);
pthread_mutex_unlock(&mutex);
sleep(1); // 模拟生产耗时
}
return NULL;
}
void* consumer(void* arg) {
while (1) {
pthread_mutex_lock(&mutex);
// 等待直到有产品可消费
while (buffer == 0) {
pthread_cond_wait(&cond, &mutex);
}
// 消费产品,这里简单地将buffer减1
buffer--;
printf("Consumer consumed %d\n", buffer);
pthread_mutex_unlock(&mutex);
sleep(2); // 模拟消费耗时
}
return NULL;
}
int main() {
pthread_t tid_producer, tid_consumer;
// 创建生产者和消费者线程
pthread_create(&tid_producer, NULL, producer, NULL);
pthread_create(&tid_consumer, NULL, consumer, NULL);
// 等待线程结束
pthread_join(tid_producer, NULL);
pthread_join(tid_consumer, NULL);
// 销毁互斥锁和条件变量
pthread_mutex_destroy(&mutex);
pthread_cond_destroy(&cond);
return 0;
}
运行结果为:
在上述的生产-消费者模型中存在一个问题,及buffer最终会变为0,生产者线程在生产了10个产品后就结束了,而消费者线程则在一个无限循环中运行,不断尝试消费产品。当所有的产品都被消费掉(即buffer
变为0),且生产者不再生产新的产品时,消费者线程将在pthread_cond_wait(&cond, &mutex);
处无限期地等待,因为没有其他线程会再次调用pthread_cond_signal(&cond);
或pthread_cond_broadcast(&cond);
来唤醒它。
则,我们可以采用以下三类方法解决上述问题:
-
使用标志位来控制线程的执行:在共享数据中添加一个标志位(如
int done;
),当生产者完成生产后,将其设置为1
。消费者线程在检查buffer
是否为0之前,也会检查这个标志位。如果done
为1
且buffer
为0
,则消费者可以安全地退出循环并结束线程。 -
让消费者线程定期检查终止条件:消费者线程可以在每次消费产品后检查一个终止条件(如一个全局变量或共享数据结构中的某个字段)。如果满足终止条件,则退出循环并结束线程。
-
使用其他同步机制:使用另一个条件变量来专门通知消费者线程何时可以安全地退出。
4.2 使用标志位来控制线程执行
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
int buffer = 0;
int done = 0; // 新增的标志位
void* producer(void* arg) {
for (int i = 0; i < 10; ++i) {
pthread_mutex_lock(&mutex);
buffer++;
printf("i = %d, Producer produced %d\n", i, buffer);
pthread_cond_signal(&cond);
pthread_mutex_unlock(&mutex);
sleep(1);
}
pthread_mutex_lock(&mutex);
done = 1; // 标记生产完成
pthread_cond_signal(&cond); // 可选:唤醒可能还在等待的消费者
pthread_mutex_unlock(&mutex);
return NULL;
}
void* consumer(void* arg) {
while (1) {
pthread_mutex_lock(&mutex);
while (buffer == 0 && !done) { // 检查done标志位
pthread_cond_wait(&cond, &mutex);
}
if (done && buffer == 0) { // 如果done为true且buffer为0,则退出循环
break;
}
buffer--;
printf("Consumer consumed %d\n", buffer);
pthread_mutex_unlock(&mutex);
sleep(2);
}
return NULL;
}
// ... main函数和其他部分保持不变 ...
运行结果为:
在这个修改后的示例中,生产者线程在生产完所有产品后将done
标志位设置为1
,并通过pthread_cond_signal(&cond);
(虽然在这个特定情况下可能不是必需的,因为消费者可能已经在等待)来唤醒可能还在等待的消费者线程。消费者线程在检查到done
为1
且buffer
为0
时,会退出循环并结束线程。
其他两种修改方法类似。