Linux系统编程6(线程互斥,锁,同步,生产消费模型)

news2024/11/25 1:46:38

上篇文章介绍完线程的概念后,我们将在这篇文章中初步探讨线程编程以及线程应用中的问题,这篇文章将以抢票系统为例,贯穿整篇文章。笔者将介绍在多线程编程中会出现的问题,什么是同步?什么是互斥?为什么多线程编程常有加锁的概念,什么又是生产者和消费者模型,读完这篇文章,你会得到相应的答案,笔者这里强烈建议各位把文中给出的demo示例自己实现一遍

多线程这部分必须要理论和实操相结合,并不像前面虚拟地址空间,页表这些理论性的知识,只要理解就可以了

目录

初步应用多线程 

互斥 

锁 

深入理解锁

封装锁

死锁 

避免死锁的方法 

线程同步 

生产者与消费者模型 

条件变量 

条件变量的相应接口 

条件变量代码实操 

理解条件变量 

基于BlockingQueue的生产者消费者模型

生产者消费者模型的意义


 

初步应用多线程 

多线程是一朵带刺的玫瑰,在享受它带来方便的同时要煞费思量多线程带来的问题

接下来咱们以一个实例来刨析使用线程究竟会带来哪些问题,咱们模拟一个抢票的场景,在main函数里创建一个次线程,然后主线程和次线程同时进行抢票,代码如下

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

int counter = 100;

void* start_routine(void* args)
{   
    while(counter > 0){
        counter--;
        std::cout << "抢票用户:次线程,剩余票数: " << counter << std::endl;
    }

    return nullptr;
}


int main()
{   
   pthread_t thread;
   pthread_create(&thread, NULL, start_routine, NULL);
    
    while ( counter > 0){
        counter--;
        std::cout << "抢票用户:主线程, 剩余票数:" << counter << std::endl;
    }

    pthread_join(thread, NULL);

    return 0;
}

代码很简单,看运行结果

可以发现,所有的票都是主线程抢到的,次线程一张票都没有抢到,这是因为次线程还没有创建完的时候,CPU不断执行主线程的循环,把票抢完了

我们要模拟出多个线程间交叉执行的情况,而不是某一个线程把所有的票都抢完了,想要模拟出这个场景,我们就得人为让CPU频繁的调度

所以,我们需要在抢票循环之前加上sleep,当某个线程准备开抢时,先休眠一会,被OS挂起,给其他线程启动的机会。但是我们不敢直接用sleep,因为该函数是以秒为单位的,以CPU的速度,如果休眠1秒,会在这个时间间隙内被其中一个线程全部抢完,所以我们要使用另一个函数——usleep

该函数和sleep一样,不过是以微妙为单位,我们调整参数到毫秒级别,这样两个线程就能够交叉抢票了,如下是修改后的代码

#include<pthread.h>
#include<iostream>
#include<cstdio>

int counter = 100;

void* start_routine(void* args)
{     
    while(true){      
        if (counter > 0){          
            counter--;
            usleep(1234);
            std::cout << "抢票用户:"<< (char*) args << " 剩余票数: " << counter << std::endl;                 
        }
        else break;
    }
    
    return nullptr;
}


int main()
{   
   pthread_t thread;
   char second_thread[] = "次线程";
   char main_thread[] = "主线程";
   pthread_create(&thread, NULL, start_routine, (void*)second_thread);
    
   start_routine((void*)main_thread);

   pthread_join(thread, NULL);

   return 0;
}

如下图是运行结果 

 

根据图上信息,可以发现,两个线程确实实现了交叉抢票,但是吧,最后剩余票数出现了-1是什么情况,两个线程正常抢票,都是进行counter > 0才能进入抢票,可为什么就出现了负数了呢?接下来刨析这个问题(再次建议读者完成代码编写,亲自感受这个过程)

互斥 

当票数counter只剩1的时候,此时次线程从内存中读取数据,发现counter为1,然后判断counter>0为真,但是一进入循环内,就遇到了usleep,于是次线程会被OS挂起,然后OS唤醒主线程,主线程去内存中加载counter数据时,发现counter的值仍为1,就通过了判断语句,同样的它将遇到usleep,被挂起。之后OS唤醒次线程,次线程开始执行counter-1,然后循环,判断counter为0,就退出了循环,接着主线程被OS唤醒,开始执行counter-1,注意此时的counter已经为0了,减去1之后就是-1,这也是为什么最后主线程的剩余票数会变为-1的原因

可以发现,造成这个问题的根本原因就是因为抢票过程不具备原子性,什么是原子性?当一件事具备了原子性,就说明这件事要么做成,要么失败,没有中间状态可言。回顾一下刚才抢票时,次线程检测到counter为1,它进入了循环,如果它具备原子性,那么对于抢票这件事,要么它抢票成功,counter减为0,要么它抢票失败,counter仍为1,轮到主线程抢票,在其它线程眼里这个过程是一瞬间,没任何中间过程的

但事实是它是有中间状态的,它不可能在物理上的一瞬间就抢到票把counter减为0,它是要逐步执行代码的,执行代码是需要时间的,就单单counter--这个操作,就至少得翻译成三条汇编语句逐条执行,在这段时间内,counter的值仍为1,其它线程就有机会进来了

除了上面出现负票这种情况,还有可能会导致票越抢越多的情况,只不过这种情况很难模拟出来,但它确实存在,故此我们理论上分析出现这种情况的原因

假设现在有100张票,有A,B两个进程一起抢票

这看似一个简单的counter-1操作,背后的CPU至少要分成这三部分完成,上面就是正常抢票过程中,CPU所执行的操作

如上图,因为A被挂起,长时间处于中间状态,B在这段时间内将票抢空,而A被唤醒后,继续执行中断前的操作,导致了票明明被B抢空了,却又变回99这种越抢越多的情况 

面对类似抢票这种场景就得保证多个线程在执行任务时要是原子的,互斥是保证原子性的手段之一,正在抢的那个线程要么抢成功,要么抢失败,而在它抢的过程中,固有的中间状态靠互斥性来解决,也就是其他线程与当前正抢票的线程保持互斥,不能打扰它抢票

如何做到互斥?解决方法就是加锁

锁 

什么是锁呢?前面提到过,抢票时我们需要对票这种共享资源进行保护,也就是抢票这个事件是要原子的,但是就单单counter--这种操作,得有三条汇编指令才能实现,这就产生了中间状态,锁就是来锁住这个中间状态,大致流程如下

当线程A正在抢票时,它需要申请一把锁,只要它申请锁成功,那么它就可以放心去执行它的抢票代码,其它的线程要想抢票也得先申请锁,但是锁已经被A拿走了,它们只能阻塞等待A释放锁才能够申请到锁

上述过程中,锁将线程A抢票的中间状态锁住了,就能够实现抢票的原子性,在其它线程看来,A抢票就是原子的,就是一瞬间,没有中间过程的。因为A拿到锁正抢票时,其它线程到了就得阻塞等待从而被OS挂起,当它们被唤醒的时候,说明A已抢完票,释放了锁,在它们眼里这可不就是一瞬间的事嘛

明白锁的作用,接下来看看怎么使用锁,锁本质也是一种数据类型,其类型为:pthread_mutex_t,使用锁之前得先申请一个锁

申请一个锁:pthread_mutex_t  mutex;

申请完锁需要对其进行初始化,使用初始化接口:

pthread_mutex_init( pthread_mutex_t  *mutex );

第二种初始化方法:pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

初始化完成之后,就可以使用锁了,使用锁分为加锁和解锁

加锁的接口为:pthread_mutex_lock( pthread_mutex_t *mutex);

解锁的接口为:pthread_mutex_unlock( pthread_mutex_t *mutex);

还有一个加锁的接口为:pthread_mutex_trylock( pthread_mutex_t *mutex);

那这两个加锁接口有什么区别呢?举个例子

有线程A,B共同使用一把锁,当使用pthread_mutex_lock加锁时,A加锁完,B再来就得阻塞等待。而使用pthread_mutex_trylock加锁时,A加锁完,B再来并不会阻塞等待,而是报错返回,因此pthread_mutex_trylock也被称为非阻塞式加锁

那在哪里使用锁呢?在main函数开头就加锁,return前解锁吗?当然不是,我们要明白加锁锁的是一种中间状态,把抢票这个具有过程性的操作给保障成原子性。原本有A,B两个线程同时并行执行,但在对公共资源进行争夺时,就会带来很多的麻烦,采用加锁相当于把这些线程在抢夺资源时排好队,谁先来的谁开始,后面的排队等待,变成一种串行的执行方式

串行排队等待,程序的效率必然会下降,因此在满足公共资源保护的前提下,加锁的范围要尽量的小,接下来通过demo来演示加锁的用法,还是用上面的抢票程序

#include<pthread.h>
#include<iostream>
#include<cstdio>
#include<unistd.h>

int counter = 10;

//创建一个全局锁
pthread_mutex_t mutex;

void* start_routine(void* args)
{     
    while(true){

        //在这里加锁,保护公共资源counter
        pthread_mutex_lock(&mutex);
        if (counter > 0){          
            counter--;
            std::cout << "抢票用户:"<< (char*) args << " 剩余票数: " << counter << std::endl;
            //这里counter--已经执行完毕,公共资源保护完成,可以解锁
            pthread_mutex_unlock(&mutex);
            usleep(1234);                             
        }
        else{
            //这里是程序拿到锁,然而没票了,退出之前要解锁
            pthread_mutex_unlock(&mutex);
            break;
        }   
    }

    
    return nullptr;
}


int main()
{   
   pthread_t thread;
   char second_thread[] = "次线程";
   char main_thread[] = "主线程";

   //初始化该全局锁
   pthread_mutex_init(&mutex, NULL);

   pthread_create(&thread, NULL, start_routine, (void*)second_thread);
    
   start_routine((void*)main_thread);

    pthread_join(thread, NULL);

    return 0;
}

由上面的demo可以看出,加锁和解锁的范围还是很短的,运行上面的demo,你会发现程序抢票的过程明显变慢,这是因为线程在抢票时是串行执行。该demo主要为了保护counter,counter--指令执行完,就可以解锁,避免长时间串行,影响程序效率

这里不给出代码运行结果,尝试自己编写运行

深入理解锁

上面锁的用法也看了,锁确实能保护我们的票目counter,但是你有没有想过,多个线程之间虽然老老实实排队去抢票了,但是它们会竞争锁呀,毕竟谁先拿到锁,谁就能优先执行,多个线程都在竞争同一把锁,那锁是什么?没错,锁也是一种公共资源,那么加锁的过程中是不是也有中间状态,那么谁来保护锁呢? 

眼前先别黑,不需要谁来保护锁,操作系统给我们保证了加锁过程是原子性的,也就是加锁过程中不存在中间状态,用一条汇编指令就能完成

实现的方法很多,这里笔者讲一个比较通用的方法

下图是加锁的部分伪代码,也是加锁的核心

我们需要有一个场景,还是用刚才抢票的那个demo,有线程A和B同时抢票 

 

这是锁能实现串行执行的原理,不过上述是线程A一路顺畅的拿到锁,有没有可能

线程A刚执行完movb $0, %al就被切走了呢

线程A刚执行到xchgb %al, mutex就被切走了呢

当然有可能,时间中断可能在任意一条汇编执行时就到来,加下来分别模拟一下这样的场景

上图同时包含了前面提到了两种场景,都没有什么问题,伪代码中最关键的还是xchgb指令,给交换提供了一个绝对原子的操作, 执行xchgb指令,该线程禁止被中断或受到其它线程干扰,对于解锁,要求就没那么高,下面是解锁的伪代码

封装锁

直接使用原生接口创建和调用锁,老是感觉不方便,毕竟是学C++的,就得用C++的方式给其原生接口封装一下,更符合现代编程的使用习惯,封装代码如下

#pragma once
#include<iostream>
#include<pthread.h>
#include<string>
#include<cstdio>
class thread_mutex{
public:
    thread_mutex(pthread_mutex_t * mut):_mutex_p(mut)
    {       
         *_mutex_p = PTHREAD_MUTEX_INITIALIZER;     
    }

    void lock()
    {      
        pthread_mutex_lock(_mutex_p);
    }

    void trylock()
    {
        pthread_mutex_trylock(_mutex_p);
    }


    void unlock()
    {
        pthread_mutex_unlock(_mutex_p);
    }

private:
    pthread_mutex_t* _mutex_p;
};



#include<unistd.h>
#include"C++_mutex.hpp"


int counter = 100;

pthread_mutex_t mutex;


void* start_routine(void* args)
{    
    thread_mutex _mutex(&mutex);

    while(true){
        _mutex.lock();

        if (counter > 0){          
            counter--;
            std::cout << "抢票用户:"<< (char*)args << " 剩余票数: " << counter << std::endl;

            _mutex.unlock();
            usleep(1234);                            

        }

        else{

            _mutex.unlock();

            break;
        }  
    }
    return nullptr;

}


int main()

{  

   pthread_t thread;

   char second_thread[] = "次线程";

   char main_thread[] = "主线程";

   pthread_create(&thread, NULL, start_routine, (void*)second_thread);

   start_routine((void*)main_thread);

   pthread_join(thread, NULL);

   return 0;

}

如果觉得解锁用的麻烦,我们可以利用智能指针的思想,写一个析构函数,把解锁接口调用放到析构函数中,等到类的作用域结束自动解锁,这里就不演示了

小技巧:可以用{ }自定义数据类型的的作用域

死锁 

锁真是个好东西啊,线程妈妈(进程)再也不怕小线程无序争夺资源,把一切都搞乱了

于是在多线程中,一旦涉及到公共资源争夺,就加一把锁,但是这么一加,就加出了问题

什么问题呢?看标题也明白了——死锁

起因是这样的,现在有公共资源A和B,公共资源A有一把锁,公共资源B也有一把锁,现在有线程1和2,线程1一开始要使用资源A,于是它把锁A给拿走了,线程2一开始要使用资源B,于是它把锁B给拿走了

线程1执行着突然发现需要资源B了,于是它等待线程2把资源B的锁还回来,不然它没法取资源B呀,可是线程2执行着发现需要资源A了,于是它等待线程1把资源A的锁还回来

线程2哪知道线程1还在等它呢?同样线程1也不知道线程2在等它呀,两个人就这样互相等啊等,没有终止,导致程序无法继续执行,这就是死锁问题

可不是只有两个及以上个锁才会造成死锁问题,一个锁也会造成死锁问题,人也会被自己绊倒,更别说一根筋的电脑了

  while(true){

拿这个例子来说,把解锁的接口全给注释了,线程A拿到锁执行完一次循环后,会再次循环,再次申请
加锁,锁本来就在它身上,加锁接口伪代码咱们也看过了,可想而知线程A也将被挂起,且锁也丢了
        _mutex.lock();
        if (counter > 0){          
            counter--;
            std::cout << "抢票用户:"<< (char*)args << " 剩余票数: " << counter << std::endl;
            //_mutex.unlock();
            usleep(1234);                             
        }
        else{
            //_mutex.unlock();
            break;
        }   
    }

避免死锁的方法 

出现死锁有四个必要条件

1.互斥(某线程拿到锁,那么其它线程要保持互斥,不能取该锁保护的资源)

2.请求与保持:(死锁时,各线程互相请求对方的锁,互不相让)

3.不剥夺:(各线程平等,无权强行夺取其它线程的锁)

4.环路等待条件 (出现死锁问题组成的无法打破僵局的环路)

避免死锁就是不要同时满足上面的四个必要条件,解决死锁的方法就是打破上面的条件之一

例如使用线程的优先级,死锁时优先级低的线程必须主动释放锁

相关的算法有(死锁检测算法,银行家算法等)

线程同步 

什么是线程同步?线程不同步会有什么问题呢?其实咱们前面遇到过这个问题,笔者还解释了大半天,遗忘的同学前往文章开头处查看,用到的代码笔者已复制如下

当时笔者解释了半天,为什么要使用usleep(),因为线程不调度,导致票被主线程这一个线程给抢完了,或者是主线程的竞争能力太强了,它在解锁时同时也是离锁最近的,所以它一解锁,立马就循环再拿锁,次线程是一次锁都摸不到

#include<pthread.h>
#include<iostream>
#include<cstdio>
#include<unistd.h>

int counter = 10;

//创建一个全局锁
pthread_mutex_t mutex;

void* start_routine(void* args)
{     
    while(true){

        //在这里加锁,保护公共资源counter
        pthread_mutex_lock(&mutex);
        if (counter > 0){          
            counter--;
            std::cout << "抢票用户:"<< (char*) args << " 剩余票数: " << counter << std::endl;
            //这里counter--已经执行完毕,公共资源保护完成,可以解锁
            pthread_mutex_unlock(&mutex);
            usleep(1234);                             
        }
        else{
            //这里是程序拿到锁,然而没票了,退出之前要解锁
            pthread_mutex_unlock(&mutex);
            break;
        }   
    }

    
    return nullptr;
}


int main()
{   
   pthread_t thread;
   char second_thread[] = "次线程";
   char main_thread[] = "主线程";

   //初始化该全局锁
   pthread_mutex_init(&mutex, NULL);

   pthread_create(&thread, NULL, start_routine, (void*)second_thread);
    
   start_routine((void*)main_thread);

    pthread_join(thread, NULL);

    return 0;
}

你说主线程违背了锁的竞争原则了吗?并没有,它是严格按照锁的竞争规则来的,但是你说这合理吗?这并不合理,主办方把票全部卖给了黄牛,即使你手速逆天,抢票时也只是卡顿一下,然后显示票已卖完

我们要避免票全让一个线程给抢完了,怎么办,简单粗暴的方法就是再加入一个排队系统,刚抢完票的线程不准再抢了,立马去排队,就能让其它的线程有机会抢到票,像这样避免一个线程独占资源而导致其它线程挨饿的就是线程同步(到这里大家应该能体会到,没写过类似的代码,看这部分内容就感觉很虚,一段时间后没有任何印象,所以请务必敲代码,亲自体验这个过程)

生产者与消费者模型 

大概了解什么是同步之后,我们来了解一个线程中很重要的模型——生产者与消费者模型

故事来源于生活场景,角色有:顾客,超市,工厂

我们每个人都是顾客,又称为消费者,我们需要商品满足自身生活需求,没有什么好说的。工厂是生产商品的,又称为生产者,有需求,就有人生产嘛,同样没有什么好说的

超市这个角色值得我们聊聊,为什么要有超市呢?

如果顾客直接去工厂买东西不可以吗?顾客作为一个单独的个体,其购买的产品量是很低的,如果每个顾客都来,那每次开机就生产那么一点点东西,赚得钱可能连电费都付不起,同样的,顾客想去买东西,还得等你工厂开机生产,等到生产完,可能天都黑了

可见我们不能把顾客与工厂强关联,需要一个中间区域,在这个中间区域里,顾客随拿随走,工厂可以批量生产有存储区域

可见,超市这个东西就是把顾客和工厂解耦的,如果生产者和消费者强耦合,消费者要等待生产时间,工厂要承担零售成本,有了超市解耦,消费者随拿随走,生产者没有零售成本

大家感觉这个超市的作用熟不熟悉? 不就是咱们前面提到过的缓冲区吗?

顾客消费可以看到超市资源,工厂供货也可以看到超市资源,那么超市不就是公共资源吗?注意指在程序里的公共资源,引入生活中的例子是便于大家理解,但不可把生活中的经验全套入到改模型里来理解该模型

下面看看消费者与生产者之间的关系

消费者与消费者之间:互斥关系

现在超市某个货品只有一个,只有一个消费者可以拿,一个拿了另一个就不能拿了,所以构成互斥关系

生产者与生产者之间:互斥关系

超市的容量是有限的,一个工厂供货了,另一个工厂就不能再供货了,故生产者与生产者之间也构成了互斥关系

生产者与消费者之间:互斥,同步

互斥还是好理解的,生产者正在给超市供货呢,还没开始统计数量呢,消费者直接就进来把商品拿走了,这样是不行的,因此消费者访问公共资源时,生产者就不能访问公共资源,反之亦然,生产者和消费者之间构成互斥关系

假设现在生产者给超市供货,而不断有消费者来购买商品,那么超市和消费者时间都浪费了,也没得到商品。同样,假设没有消费者来超市消费,而不断有生产者前来供货,那么超市和生产者时间都浪费了,也没卖出商品

为了解决这个问题,超市设置了一种同步策略,即有货的时候再通知消费者,没货时消费者不要来,同样,缺货的时候通知生产者,不缺货生产者不要来

上述生产者和消费者模型可以归纳为“321”原则

3种关系(上述的三种关系)

2个角色(生产者线程,消费者线程) 

1个交易场所(一段特定结构的缓冲区,即上述超市)

现在有main函数和一个fun函数,main函数调用了fun函数,在正常情况下,main函数调用fun函数之后,fun函数开始执行,而main函数则在等待fun函数执行完毕。同样的main函数还没开始调用fun函数时,fun函数在等待main函数的调用

这里main函数和fun函数就是强耦合的关系,把这两个函数带入到生产者消费者模型

由main函数调用fun函数,所以main函数是参数的提供方(生产者),fun函数是参数的使用方(消费者)如果由两个线程来同时执行这个函数,消费者线程和生产者线程同时开始,同时设立一定的执行条件,当消费者需要数据时,而生产者还没有生产好数据,那么就挂起等待,等到生产者生产好了,满足条件了,就可以唤醒消费者,反之同理

条件变量 

什么是条件变量呢? 我们前面提到过,在生产者和消费者模型中,生产者和消费者之间要有互斥和同步的关系,互斥解决方法就是加锁嘛,条件变量就是解决同步的一种常用的方法之一,上面的模型中,超市就是公共资源,消费者消费资源,生产者生产资源,当公共资源没有了,不满足消费者的条件,那么此时消费者不能再来了,老实的挂起等待,反之同理

条件变量维护了一个不满足条件就挂起,满足条件就唤醒的队列,条件变量要和锁一起配合使用,从而满足互斥和同步的关系

所以什么是条件变量呢?其实就是在加锁后,为了维持线程同步,加了一个判断条件,满足条件的可以继续执行,不满足条件的需要挂起等待,条件变量提供了一个队列,这个队列专门用来存放这些不满足条件的线程

消费者线程去超市这个公共资源区去消费,但是此时超市已经没有货物了,所以消费者线程不满足进入超市的条件,去到条件变量里挂起等待,假设此时生产者来了,它检测到超市的货物没有满,可以供货,于是进入了超市这个公共资源区开始供货,同时供货完,它会从条件变量里唤醒一个线程,若该线程是消费者线程,此时判断超市已经有货了,于是它可以进入超市去消费资源了

条件变量的相应接口 

条件变量的概念明白了,接下来就看看条件变量如何使用吧。有时你可能会看到笔者在接口和函数之间反复横跳,其实叫接口也好,叫函数也罢,两者是同一个东西,叫函数是从编程语言的角度看待,叫接口是从系统调用的角度来看待的,废话不多说,看接口

条件变量同锁一样,都是一种数据类型,里面包含了各种需要的数据结构 

条件变量的数据类型为:pthread_cond_t  

声明一个条件变量: pthread_cond_t  cond;

同样,声明完要进行初始化

初始化:int pthread_cond_init(pthread_cond_t *restrict cond, const pthread_condattr_t *restrict attr);第二个参数填NULL即可

另一种初始化方式:pthread_cond_t cond = PTHREAD_COND_INITIALIZER;

int pthread_cond_destroy(pthread_cond_t *cond);

这个比较好理解,就是把你创建的条件变量销毁

int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex);
参数:
cond:要在这个条件变量上等待
mutex:互斥量,后面详细解释

该接口的意思是,如果判断条件不满足,那么就调用该接口,系统会把不满足条件的线程放入到条件变量的等待区中

int pthread_cond_signal(pthread_cond_t *cond);
当满足条件之后,通过调用该接口,系统会通知在条件变量里等待的线程,可以开始执行了,注意该接口只能唤醒一个线程

int pthread_cond_broadcast(pthread_cond_t *cond);
该接口和上面接口的作用是一样的,不过该接口不只是唤醒一个等待区的线程,而是唤醒一批处在等待变量中的线程

条件变量代码实操 

废话不多说,直接用起来,下面是测试条件变量的demo

注:为了简便,后面称呼这些接口调用函数只用后缀代替

例如:pthread_cond_wait() 用wait简称,其它接口类似

int counter = 100;
pthread_mutex_t mutex;
pthread_cond_t cond;

void *start_routine(void *args)
{
    thread_mutex _mutex(&mutex);
    while (true)
    {
        _mutex.lock();
        if (counter > 0)
        {
            pthread_cond_wait(&cond, &mutex);
            counter--;
            std::cout << "抢票用户:" << (char *)args << " 剩余票数: " << counter << std::endl;
            _mutex.unlock();
            // usleep(1234);
        }
        else
        {
            _mutex.unlock();
            break;
        }
    }

    return nullptr;
}

int main()
{
    pthread_t thread_1, thread_2;
    cond = PTHREAD_COND_INITIALIZER;
    char name_1[] = "抢票选手1";
    char name_2[] = "抢票选手2";

    pthread_create(&thread_1, NULL, start_routine, (void *)name_1);
    pthread_create(&thread_2, NULL, start_routine, (void *)name_2);

    while (true)
    {
        pthread_cond_signal(&cond);
        sleep(1);
    }

    pthread_join(thread_1, NULL);
    pthread_join(thread_2, NULL);

    return 0;
}

该代码用到了前面对锁的封装,主要思想就是创建两个子线程,两个子线程刚进入抢票就被wait,进入条件变量挂起等待,然后由主线程用signal逐一唤醒挂起的进程,开始抢票

理解条件变量 

等等,老是觉得有些不太对劲,其一在使用条件变量的wait时,为什么要把锁传进去。其二我们不是加锁了嘛,既然加锁了就该保持互斥,也就是只有一个线程才能拿到锁,才能遇到wait,然后进入条件变量等待区,为什么上面的demo思想却说让两个子线程都进入条件变量挂起等待,等待主线程逐一将它们唤醒呢

我们的理解并没有错,只是前面没提到在wait时把锁传过去的作用,wait接口把锁传过去就是为了释放锁,因为释放了锁,其它线程才有机会进入公共资源区。假设现在有一个生产者线程和一个消费者线程,此时公共资源区已经没有资源了,所以消费者进来就得把它挂起等待,如果它把锁也带到等待区而不释放,那么生产者就进入不了公共资源区,那程序就卡死了,生产者永远进不去,消费者永远醒不来

基于BlockingQueue的生产者消费者模型

看标题是不是觉得特别深奥,但仔细看不就是基于阻塞队列的生产者消费者模型嘛,啥意思呢?就是建立一个队列,这个队列的大小是固定的,生产者不断往这个队列中写入数据,消费者不断往这个队列拿出数据,当写入速度明显大于拿出速度时,队列将被写满,就会触发条件判断,把生产者都放到条件变量等待区里。同样的,当拿出速度明显大于写入速度导致队列里没有数据了,会触发条件判断,将消费者放入条件变量的等待区里

了解功能大家可以自行实现,请务必自行实现,下面笔者给出一些参考代码 

#include<iostream>
#include<pthread.h>
#include<queue>
#include<vector>
#include<ctime>
#include<cstdlib>
#include<unistd.h>


template< class T>
class thread_cond{
public:
    thread_cond()
    {
        _cond = PTHREAD_COND_INITIALIZER;
    }

    void thread_cond_push(T data, pthread_mutex_t *mutex)
    {
        if (_buffer.size() >= _capacity)
        {   
            pthread_cond_wait(&_cond, mutex);
        }
        _buffer.push(data);
        pthread_cond_signal(&_cond); 
    }

    T thread_cond_get(pthread_mutex_t *mutex)
    {
        if (_buffer.size() <= 0)
        {
            pthread_cond_wait(&_cond, mutex);
        }

        T rt_val = _buffer.front();
        _buffer.pop();
        pthread_cond_signal(&_cond);
        return rt_val; 
    }

    ~thread_cond()
    {
        pthread_cond_destroy(&_cond);
    }

private:
    std::queue<T> _buffer;
    pthread_cond_t _cond;
    size_t _capacity = 100;
};



----------------------------------------------------------------------------



pthread_mutex_t mutex;

void* start_get(void *args)
{
    thread_cond<int> * test_cond = (thread_cond<int>*)args;
    while(true)
    {
        pthread_mutex_lock(&mutex);
        int temp = test_cond->thread_cond_get(&mutex);
        std::cout << "线程:"<<pthread_self() << "拿出数据" << temp <<std::endl;
        pthread_mutex_unlock(&mutex);
        sleep(1);
    }
}

void* start_push(void *args)
{
    thread_cond<int> * test_cond = (thread_cond<int>*)args;
    while(true)
    {
        pthread_mutex_lock(&mutex);
        int temp = rand()%100;
        test_cond->thread_cond_push(temp, &mutex);
        std::cout << "线程:"<<pthread_self() << "载入数据" << temp <<std::endl;
        pthread_mutex_unlock(&mutex);
        sleep(1);
    }
}

int main()
{
    thread_cond<int> test_cond;
    pthread_mutex_init(&mutex, NULL);
    std::vector<pthread_t> threads(4);
    pthread_create(&threads[0], NULL, start_get, (void*)&test_cond);
    pthread_create(&threads[1], NULL, start_get, (void*)&test_cond);
    pthread_create(&threads[2], NULL, start_push, (void*)&test_cond);
    pthread_create(&threads[3], NULL, start_push, (void*)&test_cond);

    for (int i = 0; i < 4; i++)
    {
        pthread_join(threads[i], NULL);
    }
    
    return 0;
}

用模板来编写该类,主要是为了支持泛型,方便日后工作的扩展,现在我们传过去的是int类型的数据,日后可能会是一个自定义类型的类

生产者消费者模型的意义

下图是生产者,消费者模型的简述图

你会不会有这样的疑问,我们引入多线程就是为了实现程序并发,提高工作效率。但是这个生产者消费者模型,消费者想拿数据还是只能加锁一个一个的进入,同样的生产者想放入数据也是只能加锁一个一个的进入,甚至消费者和生产者之间也不能同时进入,那这并发的意义在哪里呢?既然都是一个一个的串入式进入,为何不用单线程呢?

有这样的疑惑是因为我们正在讲述生产者消费者模型,大家都把目光聚焦到这个模型中,但是这个模型仅仅是负责资源的纳入和分配,而不涉及资源的生产和处理

什么意思呢?消费者来到公共资源区是为了拿取资源,而真正消耗时间的是资源的消费过程,但是这个过程并不是在公共资源区内部进行的

同样的,生产者把生产好的数据放入公共资源区,真正消耗时间的是资源的生产过程,这个生产过程也不是在公共资源区进行的

所以多线程并发的意义并不在公共资源区中串型式的拿取增添数据,而是并行式的消费资源和生产资源,加锁是保证资源拿取和增添的安全有效,这个过程并不怎么消耗时间

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1020567.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【nginx】(13: Permission denied) while connecting to upstream

一、问题背景 在使用nginx基于域名转发时&#xff0c;发现通过curl可以请求成功&#xff0c;但是通过外网域名访问无法访问通&#xff0c;通过日志查询发现如下问题 2023/09/18 11:26:18 [crit] 11981#11981: *106 connect() to 127.0.0.1:12500 failed (13: Permission deni…

如何理解JavaScript定时器的4种写法-附带面试题讲解

在JavaScript里&#xff0c;我们已经会使用一些原生提供的方法来实现需要延时执行的操作代码&#xff0c;比如很多在线时钟的制作&#xff0c;图片轮播的实现&#xff0c;还有一些广告弹窗&#xff0c;但凡可以自动执行的东西&#xff0c;都是可以和定时器有关的。今天就来和大…

网工内推 | 国企、上市公司,IA/IP认证即可,有年终、绩效

01 上海市机械设备成套&#xff08;集团&#xff09;有限公司 招聘岗位&#xff1a;网络工程师 职责描述&#xff1a; 1、 负责公司电脑、网络设备、电器设备、办公设备等硬件的管理、维护和使用&#xff0c;做好计算机硬件及办公设备台帐&#xff1b; 2、 负责公司办公软件的…

博客系统的页面设计

效果展示 博客列表页 博客详情页 博客登录页 博客编辑页 主要分成四个页面: 博客列表页 显示出都有哪些博客博客详情页 展示博客的详细正文博客登录页 进行登录操作博客编辑页 有一个 markdown 编译器,可以编写博客内容 1.实现博客列表页 1.背景设计 body {/*注意这里相对…

【UE5 C++ 學習日志】01. UEnhancedInput

UE5 提供了一種對複雜輸入處理或運行時重新映射按鍵增强輸入插件。此插件向上兼容UE4的默認輸入系統&#xff0c;并可以實現径向死区、同时按键、上下文输入和优先级安排等多種功能。 核心概念&#xff1a; &#xff08;1&#xff09;Input Actions&#xff1a; Input Actio…

ROS学习ROS基础

ROS学习&#xff08;一&#xff09; ROS基础 一、工作空间基本操作二、ROS通信编程——话题编程 "hello world " 例程 1、创建发布者&#xff08;talker&#xff09;2、创建订阅者&#xff08;listener&#xff09;3、添加编译选项4、运行可执行文件 自定义话题消息…

在PHP8中对数组进行排序-PHP8知识详解

在php8中&#xff0c;提供了丰富的排序函数&#xff0c;可以对数组进行排序操作。常见的排序函数如下几个&#xff1a;sort() 函数、rsort() 函数、asort() 函数、arsort() 函数、ksort() 函数、krsort() 函数、natsort()函数和natcascsort()函数。 1、sort() 函数&#xff1a;…

<C++> 红黑树模拟实现map和set

使用一颗红黑树同时封装map和set。 红黑树源码 #pragma once #include <cassert> #include <iostream> #include <utility> using namespace std;// 红黑树结点颜色 enum Colour {RED,BLACK, };template<class K, class V> struct RBTreeNode {//使用…

分析常见数据结构在内存中的存储形式

本文会在x64dbg中分析vector,list,map的内存存储特点 目录 分析vector在内存中的存储形式 x32dbg分析vector数组 总结一下vector的内存布局 分析 list 在内存中的存储形式 x32dbg分析 list 数组 总结一下 list 的内存布局 分析map在内存中的存储形式 x32dbg分析map 总…

python爬虫爬取电影数据并做可视化

思路&#xff1a; 1、发送请求&#xff0c;解析html里面的数据 2、保存到csv文件 3、数据处理 4、数据可视化 需要用到的库&#xff1a; import requests,csv #请求库和保存库 import pandas as pd #读取csv文件以及操作数据 from lxml import etree #解析html库 from …

内网穿透工具 Cpolar 帮您实现用友U8 Cloud 的外网部署,一键畅享云端ERP

文章目录 前言1. 用户需求2. Cpolar内网穿透的安装和注册2.1 Cpolar云端设置2.2 Cpolar Web UI本地设置 3. 公网访问测试 前言 用友U8 Cloud是用友公司推出的一款云端ERP解决方案。它以云计算技术为基础&#xff0c;为企业提供全面的企业资源管理解决方案&#xff0c;涵盖了财…

主机存活检测脚本

原理演示 在命令行下用下面命令安装scap模块&#xff1a; python -m pip install scapyscapy与scrapy 有非常大的区别。 scapy 是一个Python 的第三方模块&#xff0c;被称为“网络神器”。scapy 模块能够发送、捕获、分析和铸造网络数据 sr1发送接收函数 如图&#xff0c;安…

AI绘画变现渠道:日入100+,推荐一个本人实操的方法

关于AI绘画变现&#xff0c;之前写了几篇相关的文章&#xff0c;需要的自己查阅&#xff1a; AI绘画&#xff1a;如何让图片开口说话生成视频&#xff1f;变现渠道有哪些&#xff1f; 无私分享我的AI绘画变现之路&#xff0c;普通人可实操可模仿 AI壁纸号一周增加上千粉丝&a…

二叉树题目:层数最深叶子结点的和

文章目录 题目标题和出处难度题目描述要求示例数据范围 解法一思路和算法代码复杂度分析 解法二思路和算法代码复杂度分析 题目 标题和出处 标题&#xff1a;层数最深叶子结点的和 出处&#xff1a;1302. 层数最深叶子结点的和 难度 4 级 题目描述 要求 给定一个二叉树…

有效的括号(栈的高频面试题)

一、题目描述 题目连接&#xff1a;有效的括号 给定一个只包括 (&#xff0c;)&#xff0c;{&#xff0c;}&#xff0c;[&#xff0c;] 的字符串 s &#xff0c;判断字符串是否有效。 有效字符串需满足&#xff1a; 左括号必须用相同类型的右括号闭合。左括号必须以正确的顺…

我的创作纪念日 · 开始创作的第128天~

我的创作纪念日 开始创作的第128天 1️⃣ 机缘2️⃣ 收获3️⃣ 日常4️⃣ 憧憬 1️⃣ 机缘 时光匆匆&#xff0c;春去秋来&#xff0c;2023年在CSDN下笔的128天已去&#xff0c;回想当初成为创作者的初心&#xff0c;现在的心境已截然不同。当时正值上家公司工作变动&#xf…

【大数据】Doris 构建实时数仓落地方案详解(二):Doris 核心功能解读

Doris 构建实时数仓落地方案详解&#xff08;二&#xff09;&#xff1a;Doris 核心功能解读 1.Doris 发展历程2.Doris 三大模型3.Doris 数据导入4.Doris 多表关联5.Doris 核心设计6.Doris 查询优化7.Doris 应对实时数仓的痛点 1.Doris 发展历程 Apache Doris 是由 百度 研发并…

华为云云耀云服务器L实例评测|用Python的Flask框架加Nginx实现一个通用的爬虫项目

&#x1f3c6;作者简介&#xff0c;黑夜开发者&#xff0c;CSDN领军人物&#xff0c;全栈领域优质创作者✌&#xff0c;CSDN博客专家&#xff0c;阿里云社区专家博主&#xff0c;2023年6月CSDN上海赛道top4。 &#x1f3c6;数年电商行业从业经验&#xff0c;AWS/阿里云资深使用…

QUIC协议报文解析(三)

在前面的两篇文字里我们简单介绍了QUIC的发展历史&#xff0c;优点以及QUIC协议的连接原理。本篇文章将会以具体的QUIC报文为例&#xff0c;详细介绍QUIC报文的结构以及各个字段的含义。 早期QUIC版本众多&#xff0c;主要有谷歌家的gQUIC&#xff0c;以及IETF致力于将QUIC标准…