条件变量与生产者-消费者模型

目录

线程同步

条件变量

条件变量及其相关函数

pthread_cond_t

pthread_cond_init

pthread_cond_wait

pthread_cond_signal

pthread_cond_broadcast

pthread_cond_destroy

​编辑

实战应用

生产者-消费者模型


我们今天学习条件变量。

线程同步

对临界资源的管理使用锁来完成的,我们上次对锁的封装的代码确实达到了对临界资源的利用,但是如果仅仅的这样的是不行的,我们上次是一次4个线程在抢票,加锁确实做到了互斥,但是有注意到这个锁的资源分配是不平均的,会连着很多次都是同号的线程抢到,如果你将此抢票系统放在centos下运行你会看到全部抢到票的都是同一个名字的线程,为什么会这样呢?,当一个线程抢到票时会加锁抢票过程,抢完了会进行解锁(交出锁的动作),但是此时它自己也属于了外面正在等待锁的线程呀,那线程A刚刚解锁又瞬间加入了抢夺锁的线程当中,如果这个线程A的优先级非常高那每次抢到锁的都是它,这样线程A每次都是加锁,解锁,再抢到锁,再解锁...,难道线程A做错了吗,没有,但是这么多线程都在等,就线程A一个在使用这个锁,如果线程A只是频繁的加锁解锁而没有在临界区干什么事的话(假设线程A不是在用来抢票的,但是优先级特别高)这样会使得效率不高的,其他真正用来抢票的线程长时间得不到锁就会产生饥饿问题。

所以外面就需要在临界资源的外部添加一个等待队列,让正在等待的线程依次排这个队列,然后抢完票的线程不能之间参与抢票,不能立即申请抢票,而是先排在等待队列的尾部,在排在队尾之前会发出提醒,提醒排在等待队列队头一个或者多个线程进行抢夺锁资源,一般是提醒一个。这样每次线程发出提醒的时候下一次抢到锁资源和抢票的线程就不会有它了,因为这个线程还没有入队,这样多个线程访问临界资源达成了互斥的同时,由于队列元素的动态变化使得同时让多个线程访问临界资源具有顺序性,我们称这个过程叫做线程同步,线程同步的目标就是保证多个线程按正确的顺序访问共享资源。互斥保证了线程访问临界资源的安全性,安全不一定合理或者高效,线程同步主要是在保证安全的前提下,让系统变得更加合理和高效。

条件变量

条件变量(Condition Variable) 是一种线程同步机制,线程同步是使用条件变量来完成的,通常用于线程间的等待通知。它允许一个或多个线程在某个条件满足之前阻塞等待,而其他线程在条件满足时唤醒等待的线程。条件变量通常与**互斥锁(mutex)**一起使用,以确保操作的原子性。

条件变量就是等待队列+线程解锁之后的提醒机制。调用条件变量就是使用等待队列,外带每次解锁后会唤醒等待的线程。一个线程调用了条件变量就会陷入等待队列阻塞着,待条件变量再次唤醒它才接着执行陷入等待队列后面的代码,有时候线程是抱着锁在等的,所以有时条件变量会自动解锁+重新申请锁。

条件变量及其相关函数

pthread_cond_t

POSIX 线程库(pthread 中,pthread_cond_t条件变量(Condition Variable) 的类型,用于线程间的同步。 

对一个条件变量的初始化和锁一样可以使用库提供的静态的初始化方式,也可以使用调用pthread_cond_init进行动态的初始化,只不过这种动态的初始化方式需要手动销毁。

静态初始化:

pthread_cond_t cond = PTHREAD_COND_INITIALIZER;

pthread_cond_init

pthread_cond_init 用于动态初始化一个 POSIX 线程条件变量(pthread_cond_t,使其可以被多个线程使用。

这些条件变量函数的返回值都可以不用管!!!

pthread_cond_t cond;
pthread_cond_init(&cond, NULL);  // 传 NULL 使用默认属性

pthread_cond_wait

pthread_cond_wait当前线程等待条件变量的信号,并自动释放锁,直到被唤醒后重新申请锁并继续执行。在多线程编程中,pthread_cond_wait 主要用于线程同步,常用于生产者-消费者任务队列等场景。

所以pthread_cond_wait可以完成3个动作,等待信号,自动释放锁,主动申请锁。

要去哪个条件变量中等待就需要第一个参数条件变量的地址,要释放并重新申请锁就需要得到锁的地址,所以第二个参数是锁的地址,要释放锁必须先处于加锁状态。

pthread_cond_signal

pthread_cond_signal 用于唤醒一个等待条件变量的线程,常用于生产者-消费者模型等线程同步场景。

pthread_cond_signal是一次只能唤醒一个指定的条件变量中的一个等待线程,进行加锁访问临界区。

pthread_cond_broadcast

pthread_cond_broadcast 用于唤醒所有等待条件变量的线程,常用于多个线程等待同一个条件的情况,如多个消费者等待生产者的信号

使用pthread_cond_signal可以最大限度的防止对锁资源的竞争,因为一次只有一个线程被唤醒加锁。

pthread_cond_destroy

pthread_cond_destroy 用于销毁条件变量,释放与其关联的系统资源。通常在程序退出前或不再需要条件变量时调用。只有动态初始化的条件变量才需要调用pthread_cond_destroy进行销毁。

实战应用

我们使用之前封装锁的抢票程序的代码进行添加条件变量的操作。

#include<iostream>
#include<unistd.h>
#include<pthread.h>
using namespace std;
int ticket = 0;
pthread_mutex_t mut = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond = PTHREAD_COND_INITIALIZER; //初始化一个静态的条件变量

void *route(void *arg)
{
    char *id = (char *)arg;
    while (1)
    {
        pthread_mutex_lock(&mut); 
        if (ticket > 0)
        {
            usleep(1000);
            printf("%s sells ticket:%d\n", id, ticket);
            ticket--;
            pthread_mutex_unlock(&mut);
        }
        else
        {
            //pthread_mutex_unlock(&mut);  //这里进行不满足条件的解锁是浪费的,因为pthread_cond_wait会帮我们完成解锁操作
            pthread_cond_wait(&cond, &mut); //pthread_cond_wait会阻塞住当前正在等待的线程,所以当外部进行signal唤醒的时候
                                            //是从这里直接往下运行的
            printf("%s 被叫醒了\n", id);
        }
        pthread_mutex_unlock(&mut); //为什么最后还要进行先解锁再回去加锁呢
                                    //因为上面那里当一个线程被唤醒进行抢票的时候wait已经进行申请锁了
                                    //这里不先解锁的话,等上面回到的那里又进行了一次加锁就会导致重复的加锁而死锁。
                                    //我们也可以得出连续的解锁不会出bug
    }
    return nullptr;
}
int main()
{
    pthread_t t1, t2, t3, t4;
    pthread_create(&t1, NULL, route, (void*)"thread 1");
    pthread_create(&t2, NULL, route, (void*)"thread 2");
    pthread_create(&t3, NULL, route, (void*)"thread 3");
    pthread_create(&t4, NULL, route, (void*)"thread 4");
    int cnt = 10;
    while (true)
    {
        sleep(3);
        ticket += cnt;
        printf("主线程放票喽, ticket: %d\n", ticket);
        pthread_cond_signal(&cond); //每次放票的时候的唤醒一个当前在等待的线程
    }
    pthread_join(t1, NULL);
    pthread_join(t2, NULL);
    pthread_join(t3, NULL);
    pthread_join(t4, NULL);

}

解释尽在代码中。

如果没有线程处在等待队列中(条件变量),强行调用pthread_cond_signal唤醒pthread_cond_signal 不会发生任何错误,但也不会产生任何效果pthread_cond_signal 只是发送一个信号,告诉某个等待的线程“可以继续执行了”,如果没有线程在等待,这个信号就会被“丢弃”,不会被存储或累积(条件变量不会记忆过去的信号),下次有线程 pthread_cond_wait 时,它不会自动被唤醒,必须等到下次 pthread_cond_signal 发生

这里主线程如果是先唤醒子线程然后再打印,由于都是向显示器屏幕打印的,所以此时显示器屏幕变成了共享资源,此时多个线程同时打印的话会产生竞态,有概率会打印错乱。

生产者-消费者模型

应用互斥锁和条件变量的场景最出名的就是生产者消费者模型了。

生产者-消费者模型分为3个主要的实体结构构成,生产者:工厂(消费者线程),消费者:人(消费者线程),存储生产者生产的东西的空间:超市(某种结构组织的一段内存结构,就是缓存)。前面是我们日常生活的实体,后面括号内对应的就是计算机内部的结构。

生产者线程向工厂一样会源源不断的向缓存写入数据,如同工厂源源不断的向超市进货一样的,生产者的生产数据是 producer 线程自己生成的,然后缓存就像超市一样存储生产者线程生产的数据,消费者线程不会直接和生产者线程接触而是进行从缓存中拿数据进行消费,直接消费缓存中的数据,就像人去超市买东西一样,超市的东西必然减少,当然消费者不是仅仅把数据取走,可能还要分析什么的。

这样在这个模型里面生产者和消费者就共享了临界资源缓存,由于写入缓存/向缓存中取数据都是只有一个接口,所以对于多个生产者向缓存写入就形成相互互斥的关系,多个消费者从缓存中读取数据就也形成了相互互斥关系,并且读取和写入是不能同时的(这个是同步),光有互斥不行呀,还得有同步关系,生产者和消费者不可能一直向缓存写入/读取数据,所以当缓存不是满的时候缓存方会发一个信号给生产者,让其进行写入,当缓存不是空的时候缓存方会发一个信号给消费者,让其进行读取,这可以避免生产者或者消费者在条件不满足的时候频繁的访问缓存接口,反正就坐等信号到来。如果这样消费者和生产者就没有太过独立了,为了提高效率我们需要将生产者和消费者解耦,消费者一直读而生产者不写入或者反过来都会导致一方闲置太久,这样支持忙闲不均有时候不行,所以生产者和消费者的同步关系就是当生产者写入时,说明缓存中有数据,立马唤醒消费者进行读取,消费者读取完了,缓存立马可能没有数据了立马唤醒生产者进行写入,这样读写就同步了。

生产者-消费者模型支持单生产,单消费者也支持多生产者,多消费者。管道的底层逻辑就是生产者-消费者模型,总结生产者-消费者模型是多线程并发执行,里面有3种关系,生产者和生产者之间是互斥关系,消费者和消费者之间是互斥关系,生产者和消费者之间既有互斥关系,又有同步关系。2种角色,生产者和消费者,一个交易场所,某种结构组织的一段内存区域:缓存,俗称遵循"321"原则

线程的同步关系本质上就是**“不能真正同步”,只能通过互斥 + 条件变量(同步机制)协调**执行顺序。