BSP Day57

条件变量

什么是条件变量?
条件变量是利用线程间共享的全局变量进行同步的一种机制。
主要包括两个动作:一个线程等待”条件变量的条件成立”而挂起;另一个线程使”条件成立”(给出条件成立信号)。
为了防止竞争,条件变量的使用总是和一个互斥锁结合在一起。
条件变量类型为 pthread_cond_t。
条件变量有什么用?
使用条件变量可以以原子方式阻塞线程,直到某个特定条件为真为止。条件变量始终与互斥锁一起使用, 对条件的测试是在互斥锁(互斥)的保护下进行的
如果条件为假,线程通常会基于条件变量阻塞,并以原子方式释放等待条件变化的互斥锁。如果另一个线程更改了条件,该线程可能会向相关的条件变量发出信号,从而使一个或多个等待的线程执行以下操作:
唤醒
再次获取互斥锁
重新评估条件

主要应用函数

pthread_cond_init 函数。

pthread_cond_destroy函数。

pthread_cond_wait 函数。

pthread_cond_timedwait 函数。

pthread_cond_signal 函数。

pthread_cond_broadcast 函数。

以上6个函数的返回值都是,成功返回0,失败直接返回错误号

pthread_cond_t类型用于定义条件变量。

pthread_cond_t cond;

条件变量的用法

创建和注销

条件变量和互斥锁一样,都有静态动态两种创建方式,静态方式使用PTHREAD_COND_INITIALIZER常量,如下:

pthread_cond_t cond=PTHREAD_COND_INITIALIZER;

动态方式调用pthread_cond_init()函数,定义如下:

int pthread_cond_init(pthread_cond_t *cond, pthread_condattr_t *cond_attr);

//成功返回0,失败返回错误码. 

尽管POSIX标准中为条件变量定义了属性,但在LinuxThreads中没有实现,因此cond_attr值通常为NULL,且被忽略。

注销一个条件变量需要调用pthread_cond_destroy()函数,只有在没有线程在该条件变量上等待的时候才能注销这个条件变量,否则返回EBUSY。因为Linux实现的条件变量没有分配什么资源,所以注销动作只包括检查是否有等待线程。
定义如下:

int pthread_cond_destroy(pthread_cond_t *cond) ;

//成功返回0,失败返回错误码.

等待和唤醒 

两种等待方式,无条件等待pthread_cond_wait()和计时等待pthread_cond_timedwait()。接口为:

int pthread_cond_wait(pthread_cond_t *cond,pthread_mutex_t *mutex);

int pthread_cond_timedwait(pthread_cond_t *cond, pthread_mutex_t *mutex,const struct timespec *abstime);

//成功返回0,失败返回错误码.

 其中计时等待方式如果在给定时刻前条件没有满足,则返回ETIMEOUT,结束等待,其中abstime以与time()系统调用相同意义的绝对时间形式出现,0表示格林尼治时间1970年1月1日0时0分0秒。

无论哪种等待方式,都必须和一个互斥锁配合,以防止多个线程同时请求pthread_cond_wait()(或pthread_cond_timedwait())的竞争条件(Race Condition)。mutex互斥锁必须是普通锁或者适应锁,且在调用pthread_cond_wait()前必须由本线程加锁,而在更新条件等待队列以前,mutex保持锁定状态,并在线程挂起进入等待前解锁。在条件满足从而离开pthread_cond_wait()之前,mutex将被重新加锁,以与进入pthread_cond_wait()前的加锁动作对应。

pthread_cond_wait()函数的返回并不意味着条件的值一定发生了变化,必须重新检查条件的值。

阻塞在条件变量上的线程被唤醒以后,直到pthread_cond_wait()函数返回之前条件的值都有可能发生变化。所以函数返回以后,在锁定相应的互斥锁之前,必须重新测试条件值。最好的测试方法是循环调用pthread_cond_wait函数,并把满足条件的表达式置为循环的终止条件。如:

pthread_mutex_lock();

while (condition_is_false)

pthread_cond_wait();

pthread_mutex_unlock();

阻塞在同一个条件变量上的不同线程被释放的次序是不一定的。

注意:pthread_cond_wait()函数是退出点,如果在调用这个函数时,已有一个挂起的退出请求,且线程允许退出,这个线程将被终止并开始执行善后处理函数,而这时和条件变量相关的互斥锁仍将处在锁定状态。

唤醒

唤醒条件有两种形式,pthread_cond_signal()唤醒一个等待该条件的线程,存在多个等待线程时按入队顺序唤醒其中一个;而pthread_cond_broadcast()则唤醒所有等待线程。

int pthread_cond_signal(pthread_cond_t *cptr);

int pthread_cond_broadcast (pthread_cond_t * cptr);

//成功返回0,失败返回错误码.

 必须在互斥锁的保护下使用相应的条件变量。否则对条件变量的解锁有可能发生在锁定条件变量之前,从而造成死锁。

唤醒阻塞在条件变量上的所有线程的顺序由调度策略决定,如果线程的调度策略是SCHED_OTHER类型的,系统将根据线程的优先级唤醒线程。

如果没有线程被阻塞在条件变量上,那么调用pthread_cond_signal()将没有作用。

由于pthread_cond_broadcast函数唤醒所有阻塞在某个条件变量上的线程,这些线程被唤醒后将再次竞争相应的互斥锁,所以必须小心使用pthread_cond_broadcast函数。

唤醒丢失问题 

在线程未获得相应的互斥锁时调用pthread_cond_signalpthread_cond_broadcast函数可能会引起唤醒丢失问题。

唤醒丢失往往会在下面的情况下发生:

一个线程调用pthread_cond_signal或pthread_cond_broadcast函数;

另一个线程正处在测试条件变量和调用pthread_cond_wait函数之间;

没有线程正在处在阻塞等待的状态下。

条件变量的使用可以分为两部分:

等待线程

使用pthread_cond_wait前要先加锁;
pthread_cond_wait内部会解锁,然后等待条件变量被其它线程激活;
pthread_cond_wait被激活后会再自动加锁;

激活线程:

加锁(和等待线程用同一个锁);
pthread_cond_signal发送信号;
解锁;

激活线程的上面三个操作在运行时间上都在等待线程的pthread_cond_wait函数内部。

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

pthread_mutex_t count_lock;
pthread_cond_t count_nonzero;
unsigned count = 0;

void *decrement_count(void *arg)
{
    pthread_mutex_lock(&count_lock);
    printf("decrement_count get count_lock/n");
    while(count == 0)
    {
        printf("decrement_count count == 0 /n");
        printf("decrement_count before cond_wait /n");
        pthread_cond_wait(&count_nonzero, &count_lock);
        printf("decrement_count after cond_wait /n");
    }

    count = count + 1;
    pthread_mutex_unlock(&count_lock);
}

void *increment_count(void *arg)
{
    pthread_mutex_lock(&count_lock);
    printf("increment_count get count_lock /n");
    if(count == 0)
    {
        printf("increment_count before cond_signal /n");
        pthread_cond_signal(&count_nonzero);
        printf("increment_count after cond_signal /n");
    }

    count = count + 1;
    pthread_mutex_unlock(&count_lock);
}

int main()
{
    pthread_t tid1, tid2;

    pthread_mutex_init(&count_lock, NULL);
    pthread_cond_init(&count_nonzero, NULL);

    pthread_create(&tid1, NULL, decrement_count, NULL);
    sleep(2);
    pthread_create(&tid2, NULL, increment_count, NULL);

    sleep(10);
    pthread_exit(0);

    return 0;
}

运行结果:

decrement_count get count_lock

decrement_count count == 0

decrement_count before cond_wait

increment_count get count_lock

increment_count before cond_signal

increment_count after cond_signal

decrement_count after cond_wait 

例程:生产-消费者模型代码

 

#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
#include <stdio.h>
#include <string.h>
/*链表作为公享数据,需被互斥量保护*/
#define SIZE 100
int repository[SIZE]={0};//定义仓库大小,并初始化为0
/*静态初始化一个条件变量和一个互斥量*/
pthread_cond_t has_product = PTHREAD_COND_INITIALIZER;
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
int empty_repository(int arr[])
{
int i,sum=0;
for(i=0;i<SIZE;i++)
{
sum += arr[i];
}
return sum;
}
int fill_repository(int arr[])
{
int i,sum;
for(i=0;i<SIZE;i++)
{
if(arr[i]==0)
{
arr[i]= rand() % 1000 + 1;
printf("-------fill a[%d]=%d\n",i,arr[i]);
return arr[i];
}
}
return -1;
}
int del_repository(int arr[])
{
int i,temp;
for(i=SIZE;i>=0;i--)
{
if(arr[i]!=0)
{
temp=arr[i];
printf("-------del a[%d]=%d\n",i,arr[i]);
arr[i]= 0;
return temp;
}
}
return -1;
}
void *consumer(void *p)
{
for (;;)
{
int ret=pthread_mutex_lock(&lock);
if(ret !=0)
{
printf("consumer mutex_lock err:%s\n",strerror(ret));
}
while (!empty_repository(repository))
{
//如果条件不满足,释放锁,并阻塞在此处
//如果条件满足,重新加锁,并解除阻塞,进行循环条件判断
printf("cond_wait test mask\n");
pthread_cond_wait(&has_product, &lock);
}
//模拟消费掉一个产品
ret=del_repository(repository);
if(ret==-1)
{
printf("del_repository() err\n");
pthread_exit(NULL);
}
pthread_mutex_unlock(&lock);
printf("-Consume %lu---Produce id=%d\n", pthread_self(), ret);
sleep(rand() % 5);
}
}
void *producer(void *p)
{
for (; ;)
{ //sleep(5),为验证consumer线程中的,pthread_cond_wait()会进行解锁+阻塞功能
sleep(5);
//生产者拿锁成功,再生产产品
int ret=pthread_mutex_lock(&lock);
if(ret !=0)
{
printf("producer mutex_lock() err:%s\n",strerror(ret));
pthread_mutex_unlock(&lock);
break;//跳出循环
}
//模拟生产一个产品
ret=fill_repository(repository);
if(ret==-1)
{
printf("fill_repository() err\n");
pthread_mutex_unlock(&lock);
break;
// pthread_exit(NULL);//生产仓库满后,无法继续生产,退出生产线程
}
pthread_mutex_unlock(&lock);
printf("-producer %lu---Produce id=%d\n", pthread_self(), ret);
pthread_cond_signal(&has_product); //条件满足了,通知等待条件变量has_product的
线程
usleep(100000);
}
}
int main(int argc, char *argv[])
{
pthread_t tid01, tid02,tid03;
srand(time(NULL));
pthread_create(&tid01, NULL, producer, NULL);
pthread_create(&tid02, NULL, consumer, NULL);
// pthread_create(&tid03, NULL, consumer, NULL);
pthread_join(tid01, NULL);
pthread_join(tid02, NULL);
// pthread_join(tid03, NULL);
return 0;
}

信号量

信号量,是相对折中的一-种处理方式,既能保证同步,数据不混乱,又能提高线程并发。

由于互斥锁的粒度比较大,如果我们希望在多个线程间对某一对象的部分数据进行共享,使用互斥锁是 没有办法实现的,只能将整个数据对象锁住。这样虽然达到了多线程操作共享数据时保证数据正确性的目的,却无形中导致线程的并发性下降。线程从并行执行,变成了串行执行。与直接使用单进程无异。
主要函数
sem_init 函数
sem_destroy 函数
sem_wait 函数
sem_trywait 函数
sem_timedwait 函数
sem_post 函数
以上 6 个函数的返回值都是 : 成功返回 0 ,失败返回 -1 , 同时设置 errno ( 注意,它们没有 pthread 前缀 )
sem_t 类型 , 本质仍是结构体。但应用期间可简单看作为整数,忽略实现细节 ( 类似于使用文件描述符 )
sem_tsem; 规定信号量 sem 不能 <0
头文件 <semaphore.h>
信号量基本操作
sem_wait:
1. 信号量大于 0 ,则信号量 -- ( 类比 pthread_mutex_lock)
2. 信号量等于 0 时,再次调用会造成线程阻塞。
对应
sem_post : 将信号量 ++ ,同时唤醒阻塞在信号量上的线程 ( 类比 pthread_mutex_unlock) 但,由于sem 的实现对用户隐藏,所以所谓的 ++. -- 操作只能通过函数来实现,而不能直接 ++ - 符号
信号量的初值,决定了占用信号量的线程的个数。
sem_init 函数
初始化信号量
int sem_init ( sem_t * sem , int pshared , unsigned int vale )
1 sem 已定义的信号量
2 pshared 0 用于线程间同步
1 用于进程间同步
3 vale N 值,指定同时能够访问的线程数
sem_destroy 函数
销毁信号量
int sen_destroy ( sem_t * sem );
sem_wait 函数
信号量减减操作 ( 类似加锁 )
int sem_wait ( sen_t * sem );

sem_post函数

信号量加加操作(类似解锁)

 int sem_post(sen_t *sem);

sem_timedwait 函数
限时尝试对信号量加锁--
int sem_timedwait ( sem_t * sem , const struct timespec * abs_timeout );
2 : abs_timeout 采用的是绝对时间( 1970 1 1 0 0 0 秒)。
比如定时 1 :
time_t cur = time ( NULL ); 获取当前时间
struct timespec t ;
定义 timespec 结构体变量 t
t . tv_sec = cur + 1 ; 定时 1 秒。
t . tv_nsec = t . tv_sec + 100 ;
sem_timedwait ( & sem , & t ); 传参

例程:生产-消费者模型,使用信号量

#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
#include <stdio.h>
#include <semaphore.h>
#define NUM 5
int queue[NUM];//全局数组实现环形队列
sem_t blank_number, product_number ;//空格子信号量,产品信号量
void *producer(void *arg)
{
int i=0;
while (1)
{
sem_wait(&blank_number);//生产者将空格子数-- ,为0则阻塞等待
queue[i] = rand() % 1000 + 1;//生产一个产品
printf("----Produce---%d\n",queue[i]);
sem_post(&product_number);//将产品数++
i=(i+1)%NUM;//借助下标实现环形
sleep( rand()%1);
}
}
void *consumer(void *arg)
{
int i=0;
while (1)
{
sem_wait(&product_number);//消费者将产品数--,为则阻塞等待
printf("-Consume---%d\n" , queue[i]);
queue[i] = 0;//消费一个产品
sem_post(&blank_number);//消费掉以后,将空格子数++
i = (i+1) % NUM;
sleep(rand( )%3);
}
}
int main(int argc, char *argv[])
{
pthread_t pid, cid;
sem_init(&blank_number, 0, NUM); //初始化线程间共享-0,空格子信号量为5,
sem_init(&product_number, 0,0);//初始化线程间共享-0,产品数为0
pthread_create(&pid, NULL, producer, NULL);
pthread_create(&cid, NULL, consumer, NULL);
pthread_join(pid, NULL);
pthread_join(cid, NULL);
sem_destroy(&blank_number );
sem_destroy(&product_number);
return 0;
}

 

 

 

 

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

weixiaxiao

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值