一个使用pthread_cond_t条件变量的完整示例

时间:2021-05-01 11:02:25

/*
 * test3.c 
 * Copyright 2016 Che Hongwei <htc.chehw@gmail.com>
 * 
 * The MIT License (MIT)
 * 
 */

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
#include <time.h>

#define N 6

/* ************************
 * 在使用条件变量时,尽可能地创建一个单独的互斥锁,将其仅用于同步条件变量cond,
 * 假使为了省事,将该mutex用在了其他地方,如果实现的逻辑有出了差错,可能会出现不可预知的结果
 * 为此,下面自定义了一个结构体,将cond和mutex强行捆绑在一起,避免误用
 * */
typedef struct cond_mutex
{
pthread_cond_t c;
pthread_mutex_t m;
}cond_mutex_t;

/* ************************
 * 初始化 互斥锁 和 条件变量 
 * */
cond_mutex_t cm = 
{
PTHREAD_COND_INITIALIZER,
PTHREAD_MUTEX_INITIALIZER
};
 
 
static void * wait_thread(void * param);   // 等待线程
static void * worker_thread(void * param); // 工作者线程

// 用一个mutex来同步对count的操作
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
static int count = 0;
static int data[N];

unsigned int seed;
volatile int quit = 0; // notify threads to quit

int main(int argc, char ** argv)
{
int rc;
int i;
#define NUM_WORKERS (4)
pthread_t th[NUM_WORKERS + 1]; // 1个等待线程、NUM_WORKERS个工作者线程
seed = time(NULL);

void * ret_code = NULL;

// 创建等待线程
rc = pthread_create(&th[0], NULL, wait_thread, NULL);
if(0 != rc)
{
perror("pthread_create");
exit(1);
}
usleep(5000); // 5 ms

// 创建工作者线程
for(i = 1; i <= NUM_WORKERS; ++i)
{
rc = pthread_create(&th[i], NULL, worker_thread, NULL);
if(0 != rc)
{
perror("pthread_create");
exit(1);
}
}

char c = 0;
printf("press enter to quit.\n");
while(1)
{
scanf("%c", &c);
if(c == '\n') break;
}

quit = 1;
pthread_cond_broadcast(&cm.c); // 激活在所有线程中正在等待中的条件变量

pthread_join(th[0], &ret_code);
for(i = 1; i <= NUM_WORKERS; ++i)
{
pthread_join(th[i], &ret_code);
}


rc = (int)(long)ret_code;

pthread_mutex_destroy(&mutex);
pthread_cond_destroy(&cm.c);
pthread_mutex_destroy(&cm.m);
return rc;
}

static void * worker_thread(void * param)
{
while(!quit)
{
pthread_mutex_lock(&mutex); // 先加锁之后,再修改data和count值

if(quit) // 有可能在锁定等待期间被用户强行退出
{
pthread_mutex_unlock(&mutex);
break;
}
if(count == N) //某一个【工作者线程】已经触发了条件变量,但【等待线程】还没来得及加锁
{
printf("正在与【等待线程】争抢资源...\n");
pthread_mutex_unlock(&mutex);
usleep(10000); // sleep 10 ms, 给其他线程一个机会
continue;
}
//~ #ifdef _DEBUG
if(count > N) // 应该不可能发生,仅供调试时使用
{
fprintf(stderr, "同步的逻辑出现问题,请检查源代码。\n");
printf("请按回车建退出...\n");
quit = 1;
pthread_mutex_unlock(&mutex);
break;
}
//~ #endif

data[count++] = rand_r(&seed) % 1000;

if(count == N)
{
pthread_mutex_lock(&cm.m);
pthread_cond_signal(&cm.c);
pthread_mutex_unlock(&cm.m);
}
pthread_mutex_unlock(&mutex); // 解锁
usleep(100000); // 100 ms; 人为故意地延迟一下,模拟一下真实场景可能需要的工作量。
}
pthread_exit((void *)(long)0);
}

static void * wait_thread(void * param)
{
int i, sum;
pthread_mutex_lock(&cm.m);
while(!quit)
{
pthread_cond_wait(&cm.c, &cm.m);
if(quit) break; //有可能是在等待期间被用户干预,强制退出的(在while中判断时quit可能还是0)

pthread_mutex_lock(&mutex); // 先上锁,避免其他线程修改 data 和 count 值
sum = 0;
printf("average = (");
for(i = 0; i < N; ++i)
{
sum += data[i];
printf(" %3d ", data[i]);
if(i < (N - 1)) printf("+");
}


printf(") / %d = %.2f\n", N, (double)sum / (double)N);
count = 0;
pthread_mutex_unlock(&mutex);

} // end while

pthread_mutex_unlock(&cm.m);

pthread_exit((void *)(long)0);
}



=======================================
简要说明

条件变量的作用是,仅当某个特定值满足某个条件时才执行后续的操作。

考虑下面这种情况:假设在线程1中,只有当(count > N) 时才需要执行后续操作,但这个count值是由其他线程(可能有多个)修改的,本线程并不知道什么时候才能满足条件。
如果需要后续的操作能及时响应,假使不使用条件变量,那么需要在一个循环中不断检测count值,这将极大的消耗CPU资源;但如果用sleep等待,有可能会导致后续操作延误,而使任务无法及时完成。

为了解决上面这种问题,于是引入了条件变量,在线程1中用下面这种方式来等待:
pthread_cond_wait(&cond, &mutex);
只要不满足预定的条件,那么上面这行将一直保持阻塞状态,不会占用任何CPU资源。

================
其他线程:
当操作count值的另外某个线程执行到另count = N时,
1. 先给mutex加锁,用于保护cond变量,确保不会有其他线程重复触发条件激活的信号
pthread_mutex_lock(&mutex);
2. 发个信号,告知条件就绪
pthread_cond_signal(&cond);  // 此时,pthread_cond_wait会激活,线程1开始支持后续操作
3. 解锁
pthread_mutex_lock(&mutex);
-----------------------

线程1:
当pthread_cond_wait执行时,在该函数内部会执行如下的原子操作:
检查cond状态,
a. 如果cond出于阻塞状态,先解锁mutex,并将该线程挂起在后台;
b. 如果cond激活,那么就加锁mutex,并完成此次等待操作,允许执行线程1后续的工作。

具体流程是:

// 必须先加锁,才能执行pthread_cond_wait,否则会出现不可预知的结果
pthread_mutex_lock(&mutex);  
 
while(1) // 如果需要反复执行
{
        pthread_cond_wait(&cond, &mutex);  // 该行执行后,若cond为阻塞状态,则mutex将解锁
        // 由于(b)步骤的操作,mutex此时出于加锁状态
        // do some work
       
       // finish work
       if (满足某个终止条件)break;
       // 由于是个循环,此时会返回到pthread_cond_wait行,注意此时mutex还是加锁状态
}
pthread_mutex_unlock(&mutex); // 如果因为某些原因直接跳出了循环,需要解开被pthread_cond_wait加的锁

======================================

在使用条件变量时有个注意事项,即必须确保线程1中的 pthread_cond_wait已经执行了之后,才能在其他线程中执行pthread_cond_sig操作,否则会出现不可预知的结果(我以前曾遇到过这种情况,找了很久才发现原因)。
可以在用pthread_create创建了线程1后。先 usleep几毫秒,然后在创建其他工作线程,这样可以确保取得预期的效果。

3 个解决方案

#1


收藏一下吧,多谢

#2


多谢大神啊,mark一下

#3


mutex和cm的加锁顺序不一致难道不会死锁吗?

#1


收藏一下吧,多谢

#2


多谢大神啊,mark一下

#3


mutex和cm的加锁顺序不一致难道不会死锁吗?