【Linux】多线程相关知识

本文围绕Linux线程展开,介绍了线程概念,指出Linux用进程模拟线程。阐述线程相关函数,如创建、等待、分离等。还讲解多线程独立和共享内容,以及同步与互斥机制,包括互斥锁、死锁解决办法。最后介绍生产消费模型和信号量,分析模型优点和效率体现。

线程相关概念

何为线程

在一个程序里的一个执行路线就叫做线程。而更准确的定义是:线程是“一个进程内部的控制序
列”,因此一个进程至少拥有一个执行线程.

但是在LINUX下是没有真正意义的线程,LINUX是用进程来模拟出来的线程,在通过原生线程库——pthread库 进行接口的封装为上层用户提供 诸多对线程(轻量级进程)的操作的接口。而在windows操作系统下则是拥有正真的线程。

线程相关的函数

1、pthread_create

功能:创造一个新线程
返回值:成功返回0失败则返回错误码
在这里插入图片描述
在这里插入图片描述
例子:创建一个线程求1~10的总和并输出总和线程ID

#include <iostream>
#include <unistd.h>
#include <pthread.h>
using namespace std;

void* func(void* args)
{
    long long n = (long long)args;
    //不用int强转是因为linux地址大小为8个字节,int会精度丢失

    int sum=0;
    while(n)
    {
        sum+=n--;
    }
    std::cout << sum << endl;
    return nullptr;
}

int main()
{
    pthread_t t;
    pthread_create(&t,nullptr,func,(void*)10);
    sleep(1);

    std::cout<<t<<endl;//输出线程ID

    sleep(2);
    return 0;
}

输出:
在这里插入图片描述
我这上述的代码写的不够完善,新线程执行结束需要被主线程等待否则会出现类似与僵尸进程的问题,而主线程等待新线程需要调用pthread_join函数

2、pthread_join

功能:等待进程结束(若不等待则已经退出的线程,其空间没有被释放,仍然在进程的地址空间内)
返回值:成功返回0,失败返回错误码
在这里插入图片描述
第一个参数:要被等待的线程ID
第二个参数:是输出型参数,能够得到被等待线程的返回值(理解pthread_create中第三个参数,函数指针指向的函数返回值为void* 所以要得到该参数类型必须是void* 而又要能够修改void*类型所以输出型参数retval必须取地址才可以及void**类型)

例子:

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

using namespace std;

void* func(void* args)
{
    long long n = (long long)args;
    //不用int强转是因为linux地址大小为8个字节,int会精度丢失

    long long  sum=0;
    while(n)
    {
        sum+=n--;
    }
    return (void*)sum;
}

int main()
{
    pthread_t t;
    pthread_create(&t,nullptr,func,(void*)10);
    sleep(1);

    void *ret=nullptr;
    pthread_join(t,&ret);

    std::cout<<(long long)ret<<endl;
    std::cout<<t<<endl;//输出线程ID

    return 0;
}

输出结果:
在这里插入图片描述
注意:等待线程是以阻塞等待的方式等待的

若主线程并不关心线程的返回值且又能使线程退出后自己释放资源,这时候就可以用到分离线程pthread_detach函数,当然主线程不会再阻塞等待线程的退出自己该干嘛就干嘛

3、pthread_detach

功能:分离线程
返回值:成功返回0,失败返回错误码

注意:分离只是一个线程的属性,默认情况下新创建的线程的属性是joinable的,当线程退出后需要对其进行pthread_join等待操作,否则无法释放资源从而造成系统泄漏。
而当pthread_detach一个线程后该线程joinable属性即被设置为分离,这时候主线程无需再进行等待,线程退出后自动释放资源。(joinable和分离是冲突的,一个线程不能既是joinable又是分离的

在这里插入图片描述
参数:进行分离操作的线程ID

例子:joinable和分离是冲突的,一个线程不能既是joinable又是分离的

#include <iostream>
#include <unistd.h>
#include <pthread.h>
#include <string.h>
using namespace std;

void* func(void* args)
{
    while(true)
    {
        std::cout<<"new threadID:"<<pthread_self();
        sleep(1);
    }
    return nullptr;
}

int main()
{
    pthread_t t;
    pthread_create(&t,nullptr,func,nullptr);
    pthread_detach(t);

    void* ret=nullptr;
    int n = pthread_join(t,&ret);
    if(n!=0)
    {
        std::cerr<<"error"<<n<<":"<<strerror(n)<<std::endl;
    }
    return 0;
}

在这里插入图片描述
且可以是线程组内其他线程对目标线程进行分离,也可以是线程自己分离:

#include <iostream>
#include <unistd.h>
#include <pthread.h>
#include <string.h>
using namespace std;

void* func(void* args)
{
     pthread_detach(pthread_self());
     sleep(2);
     int cnt=5;
    while(cnt--)
    {
        std::cout<<"new threadID:"<<pthread_self()<<endl;
        sleep(1);
    }
    return nullptr;
}

int main()
{
    pthread_t t;
    pthread_create(&t,nullptr,func,nullptr);

    sleep(1);//很重要,要让线程先分离,再等待
             //不然joinable属性还未被更改成分离,主线程执行pthread_join时已经等待该线程
    void* ret=nullptr;
    int n = pthread_join(t,&ret);
    if(n!=0)
    {
        std::cerr<<"error"<<n<<":"<<strerror(n)<<std::endl;
    }
    
    return 0;
}

在这里插入图片描述
注意:什么时候适合分离不需要join线程,如果不关心线程的返回值即可以分离该线程毕竟join也是一种负担

4、pthread_exit

功能:终止调用的线程
返回值:void
在这里插入图片描述
参数:返回值,并也能够被pthread_join的第二个输出型参数所得到
例子:

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

using namespace std;

void* func(void* args)
{
    long long n = (long long)args;
    //不用int强转是因为linux地址大小为8个字节,int会精度丢失

    long long  sum=0;
    while(n)
    {
        sum+=n--;
    }
    pthread_exit((void*)sum);
    //return (void*)sum;
}

int main()
{
    pthread_t t;
    pthread_create(&t,nullptr,func,(void*)10);
    sleep(1);

    void *ret=nullptr;
    pthread_join(t,&ret);

    std::cout<<(long long)ret<<endl;
    std::cout<<t<<endl;//输出线程ID

    return 0;
}

输出结果和return一样

5、pthread_cancel

功能:取消一个执行中的线程
返回值:成功返回0,失败返回错误码
在这里插入图片描述
参数:取消执行中线程的线程ID
例子:并理解打印为何为-1

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

using namespace std;

void* func(void* args)
{
    long long n = (long long)args;
    //不用int强转是因为linux地址大小为8个字节,int会精度丢失

    long long  sum=0;
    while(n)
    {
        sum+=n--;
        sleep(1);//注意新增
    }
    pthread_exit((void*)sum);
}

int main()
{
    pthread_t t;
    pthread_create(&t,nullptr,func,(void*)10);
    sleep(1);

    pthread_cancel(t);

    void *ret=nullptr;
    pthread_join(t,&ret);

    std::cout<<(long long)ret<<endl;
    std::cout<<t<<endl;//输出线程ID

    return 0;
}

输出:
在这里插入图片描述
这里可以看到当线程在执行中被取消时候,会把-1给返回出来。

6、pthread_self

功能:可以获得线程自身的ID
返回值:线程ID

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

using namespace std;

void* func(void* args)
{
    while(true)
    {
        std::cout<<"new threadID:"<<pthread_self();
        sleep(1);
    }
    return nullptr;
}

int main()
{
    pthread_t t;
    pthread_create(&t,nullptr,func,nullptr);
    sleep(1);
    while(true)
    {
        std::cout<<" main threadID:"<<t<<endl;
        sleep(1);
    }

    return 0;
}

输出:
在这里插入图片描述

多线程独立和共享的内容

独立的内容:
1 栈和线程ID
当线程在执行对应的入口函数时,若形成临时变量,那么就会有数据需要入栈和出栈,而在进程地址空间中只有一块栈,若多个线程在不断调度,势必会使数据混乱因此每个线程都有自己独立的栈结构(也体现了线程是一个独立的执行流,临时变量不会相互干扰)

2 一组寄存器
多线程在切换调度的过程中,会有寄存器来保存上下文的数据。

共享的内容:
多线程间大部分的内容都可以做到共享 理解:因为多线程都看到的是同一块进程地址空间

多线程同步与互斥

互斥

概念理清:共享资源 临界资源 临界区 互斥

共享资源:类似定义一个全局变量,该全局变量即共享资源 本质即所有线程都能够看到的资源
临界资源:对共享资源进行保护则该资源称为临界资源(共享资源+保护)
临界区:访问临界资源的这片代码称为临界区
互斥:任何时刻,互斥保证有且只有一个执行流进入临界区,访问临界资源,对临界资源起保护作用(保证对共享资源串行访问)

例子:对共享资源不加以保护
模拟一个抢票系统共有四个线程一起抢

#include <iostream>
#include <unistd.h>
#include <pthread.h>
#include <string.h>
using namespace std;

int tickets=1000;
void* demo(void * arg)
{
    char* name = (char*)arg;

    while(true)
    {
        if(tickets>0)
        {
            usleep(2000);//抢票时间
            cout<<name<<" get a ticket "<<tickets--<<endl;
        }
        else
        {
            break;
        }
    }
    return nullptr;
}
int main()
{
    pthread_t t[4];
    int n = sizeof(t)/sizeof(t[0]);
    for(int i =0;i<n;i++)
    {
        char*data = new char[64];
        snprintf(data,64,"thread->%d",i+1);
        pthread_create(t+i,nullptr,demo,data);
    }
    for(int i =0;i<n;i++)
    {
        pthread_join(t[i],nullptr);
    }
}

输出最后结果:
在这里插入图片描述
理解:为什么票数会输出负数,不应该当tickets>0不满足时候退出循环吗
解释:本质对共享资源的并行执行,共享资源tickets=1时候,四个执行流在执行if(tickets>0)时同时即并行访问共享资源导致四个执行流同时进入if为真的判断语句中,因此会打印出负数

那么该如何解决?
对共享资源进行加锁操作在Linux上提供的这把锁叫互斥量
在这里插入图片描述

#include <iostream>
#include <unistd.h>
#include <pthread.h>
#include <string.h>
using namespace std;

int tickets=1000;
pthread_mutex_t mutex;//定义一把锁
//pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER 方法二:无需再初始化和销毁 使用于全局定义的锁
void* demo(void * arg)
{
    char* name = (char*)arg;

    while(true)
    {
        pthread_mutex_lock(&mutex);
        if(tickets>0)
        {
            usleep(2000);
            cout<<name<<" get a ticket "<<tickets--<<endl;
            pthread_mutex_unlock(&mutex);
        }
        else
        {
            pthread_mutex_unlock(&mutex);
            break;
        }
        usleep(1000);
    }
    return nullptr;
}
int main()
{
    
    pthread_mutex_init(&mutex,nullptr);//创建线程前初始化

    pthread_t t[4];
    int n = sizeof(t)/sizeof(t[0]);
    for(int i =0;i<n;i++)
    {
        char*data = new char[64];
        snprintf(data,64,"thread->%d",i+1);
        pthread_create(t+i,nullptr,demo,data);
    }
    for(int i =0;i<n;i++)
    {
        pthread_join(t[i],nullptr);
    }
    pthread_mutex_destroy(&mutex);//最后销毁锁
    return 0;
}

输出执行:加锁后使得访问共享资源时候只能互斥访问(串行)所以速度也变慢
**在这里插入图片描述**
在对共享资源加锁时,需要注意2个细节点:
1 访问同一个共享资源时候,若加锁则必须使用同一把锁,做到互斥
2 因为加锁后是串行访问,所以临界区的代码尽量不要有冗余,粒度要细。(效率增加)

对共享资源加锁的本质是如何?多线程中进行加锁都需要所有线程先看见这同一把锁,那么锁不也是一个共享资源如何确保其安全访问?
伪代码理解:
为了实现互斥锁操作,大多数体系结构都提供了swap或exchange指令,该指令的作用是把寄存器和内存单元的数据相交换,由于只有一条指令,保证了原子性,即使是多处理器平台,访问内存的 总线周期也有先一个处理器上的交换指令执行时另一个处理器的交换指令只能等待总线周期。
在这里插入图片描述
在这里插入图片描述
解锁操作同理

若寄存器的值为1 即lock加锁成功,若为0则申请加锁的线程阻塞时等待,并会再去重复lock代码,但是若加锁成功后的线程还没有归还锁即“1”再怎么exchange都不会使得寄存器里的al变为1 只能等待再申请。

也因为exchange 寄存器和内存单位的值为一条指令,所以确保了加锁的原子性要么加锁成功要么没成功,也不会在意执行lock指令时被切换造成的问题

也因为使用的是exchange方式所以不会造成锁这个“1”会平白无故的增加,所以即使加锁成功的线程被切换也不会造成任何影响,理解:线程会带着它存放再寄存器中的上下文数据离开并保护,知道下次被调度再把上下文替换进寄存器中。

为了保护共享资源给其上了把锁,但是在加了锁之后可能会导致死锁
死锁概念:死锁是指在一组进程中的各个进程均占有不会释放的资源,但因互相申请被其他进程所站用不会释放的资源而处于的一种永久等待状态

造成死锁问题的四个必要条件
1、互斥条件:一个资源每次只能被一个执行流使用
2、请求与保持条件:一个执行流因请求资源而阻塞时,对已获得的资源保持不放
3、不剥夺条件:一个执行流已获得的资源,在末使用完之前,不能强行剥夺
4、循环等待条件:若干执行流之间形成一种头尾相接的循环等待资源的关系

解锁死锁的方式(核心就是破坏产生死锁问题的任意四个必要条件)
1、不加锁,破坏互斥
2、主动释放锁(当请求多次资源失败后,主动释放已获得的资源)
3、强行剥夺锁 (一个线程申请到了一把锁,可以跨线程释放该锁。即线程A申请锁1可以被主线程释放锁1)
4、按照顺序申请锁(破坏循环等待,若你已经申请到第一把锁,那么第二把锁必然可以被申请到)

例子:理解解决死锁方式3和只有一把锁也可能造成死锁

#include <iostream>
#include <unistd.h>
#include <pthread.h>
#include <string.h>
using namespace std;

pthread_mutex_t mutex=PTHREAD_MUTEX_INITIALIZER;

void* active(void*a)
{
    pthread_detach(pthread_self());
    std::cout<<"I am a new thread"<<endl;
    pthread_mutex_lock(&mutex);
    std::cout<<"I am got a lock"<<endl;
    pthread_mutex_lock(&mutex);//造成死锁,一直等待锁的资源
    std::cout<<"I'm free.."<<endl;
}

int main()
{
    pthread_t tid;
    pthread_create(&tid , nullptr,active,nullptr);

    sleep(3);
    pthread_mutex_unlock(&mutex);//跨线程释放锁

    std::cout<<"main thread unlock..."<<endl;
    sleep(3);
    return 0;
}

输出内容:
理解:因为主线程跨线程释放了锁所以线程tid能够打印输出接下上来的语句
在这里插入图片描述

同步

同步概念:在保证数据安全的前提下,让线程能够按照某种特定的顺序访问临界资源,从而有效避免饥饿问题(防止一个线程 在某种条件不满足情况下 不断的释放锁和申请锁的操作,因为即使申请到锁也是无用功,不如直接让他休眠在等到条件满足时在将它唤醒)

同步中也有些函数和互斥有相似处,1、申请条件变量对象 2、对条件变量初始化 3、等待 4、唤醒 5、销毁 总体是这五步骤。

其中最重要的是理解条件变量对象是什么,就把他理解称为一个等待队列, 当等待条件满足时候把满足条件的线程切换进 条件变量的等待队列中使得该线程休眠,当线程被唤醒时候再把它切换回切换走的上下文中去继续调度

下面的例子:主要用于学习接口

#include <iostream>
#include <unistd.h>
#include <pthread.h>
#include <string.h>
using namespace std;
//设计初始票数为1000 每五秒钟放500张票的系统
//最后统计每个线程抢了几张票

int tickets=1000;
pthread_mutex_t mutex;
pthread_cond_t cond;//cond 条件变量对象
void* demo(void * arg)
{
    char* name = (char*)arg;

    int cnt=0;
    int signalC=0;
    while(true)
    {
        pthread_mutex_lock(&mutex);

        while(tickets<=0)
        {
            pthread_cond_wait(&cond,&mutex);

            //计算唤醒次数 ,唤醒次数等于放票次数+1前提是抢票时间充裕不然还没进入等待状态唤醒等于无效
            signalC++;
            if(signalC==4)
            {
                sleep(1);
                
                //在线程退出时候必须归还锁,不然造成死锁问题
                pthread_mutex_unlock(&mutex);
                return (void*)cnt;
            }
        }
        //到这里肯定有票
        cnt++;
        std::cout<<name<<":"<<tickets--<<std::endl;
        pthread_mutex_unlock(&mutex);
        usleep(8000);
    }
    return (void*)cnt;
}
int main()
{
    
    pthread_mutex_init(&mutex,nullptr);//创建线程前初始化
    pthread_cond_init(&cond,nullptr);//初始化条件变量

    pthread_t t[4];
    int n = sizeof(t)/sizeof(t[0]);
    for(int i =0;i<n;i++)
    {
        char*data = new char[64];
        snprintf(data,64,"thread->%d",i+1);
        pthread_create(t+i,nullptr,demo,data);
    }

    //i放票次数
    for(int i =0;i<3;i++)
    {
        sleep(3);
        tickets+=500;
        pthread_cond_broadcast(&cond);//唤醒全部在条件变量等待队列中的线程
    }
    sleep(2);
    pthread_cond_broadcast(&cond);

    
    for(int i =0;i<n;i++)
    {;
        void*ret=nullptr;
        pthread_join(t[i],&ret);
        std::cout<<"thread->"<<i+1<<"抢了"<<(long long)ret<<"张票"<<std::endl;
    }

    pthread_mutex_destroy(&mutex);//最后销毁锁
    pthread_cond_destroy(&cond);
    return 0;
}

输出内容
在这里插入图片描述

1 条件变量函数初始化
int pthread_cond_init(pthread_cond_t *restrict cond,const pthread_condattr_t *restrictattr);
参数:
cond:要初始化的条件变量
attr:NULL

2 销毁
int pthread_cond_destroy(pthread_cond_t *cond)

3 等待条件满足
int pthread_cond_wait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex);
参数:
cond:要在这个条件变量上等待
mutex:互斥量(wait函数具有释放锁和申请锁的能力,具体看下面的生产消费模型)

4 唤醒等待
int pthread_cond_broadcast(pthread_cond_t *cond); (把在等待队列中的线程全部唤醒)
int pthread_cond_signal(pthread_cond_t *cond);(唤醒在等待队列中的一个线程)

生产消费模型

理解基于BlockingQueue的生产者消费者模型
在多线程编程中阻塞队列(Blocking Queue)是一种常用于实现生产者和消费者模型的数据结构。其与普通的队列区别在于,当队列为空时,从队列获取元素的操作将会被阻塞,直到队列中被放入了元素;当队列满时,往队列里存放元素的操作也会被阻塞,直到有元素被从队列中取出(以上的操作都是基于不同的线程来说的,线程在对阻塞队列进程操作时会被阻塞)

代码:
blockQueue.hpp

#pragma once

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

const int gcap=5;

template <class T>
class BlockQueue
{
public:
    BlockQueue(const int cap = gcap):_cap(gcap)
    {   
        pthread_mutex_init(&_mutex,nullptr);
        pthread_cond_init(&_consumerCond,nullptr);
        pthread_cond_init(&_productorCond,nullptr);

    }
    bool isFull(){return _q.size()==_cap;}
    bool isEmpty(){return _q.empty();}
    void push(const T &data)
    {
        //队列必定是共享资源所以访问需加锁保护
        pthread_mutex_lock(&_mutex);

        //if(isFull())
        while(isFull())//不用if避免多个生产线程被唤醒导致即使队列已经满了还继续向下执行 
        {
            //当生产满上限时,让线程进行休眠等待(切换),因此不能持有锁
            //所以pthread_cond_wait必定要有释放锁的能力
            pthread_cond_wait(&_productorCond,&_mutex);
            //当线程醒来的时候,注定了继续从临界区内部继续执行,把切走时候保护的上下文切换回来,因为该线程是在临界区被切走的
            //所以注定了当线程被唤醒时需要再去申请锁,这也是通过pthread_cond_wait函数内部的代码实现的,只有申请成功才能继续向后运行
        }
        //走到这里,队列必定没满就进行生产
        _q.push(data);

        //唤醒 push进去数据一定意味者队列不为空能够被消费所以唤醒
        pthread_cond_signal(&_consumerCond);
        pthread_mutex_unlock(&_mutex);

    }
    void pop(T*out)//out是输出型参数
    {
        pthread_mutex_lock(&_mutex);

        //if(isEmpty())
        while(isEmpty())
        {
            pthread_cond_wait(&_consumerCond,&_mutex);
        }
        *out=_q.front();
        _q.pop();

        //唤醒 pop一个数据出去,一定意味着队列不为空能够继续生产所以唤醒
        pthread_cond_signal(&_productorCond);
        pthread_mutex_unlock(&_mutex);

    }
    ~BlockQueue()
    {
        pthread_mutex_destroy(&_mutex);
        pthread_cond_destroy(&_consumerCond);
        pthread_cond_destroy(&_productorCond);

    }
private:
    std::queue<int> _q;
    int _cap;
    pthread_mutex_t _mutex;
    pthread_cond_t _consumerCond;//消费者条件变量,若队列空,则需要把线程切入阻塞队列进行等待
    pthread_cond_t _productorCond;//生产者条件变量,若队列满,则需要把线程切入阻塞队列进行等待

};

main.cc

#include "blockQueue.hpp"

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

void *consumer(void *args)
{
    BlockQueue<int> *bq = (BlockQueue<int> *)args;

    while (true)
    {
        sleep(1);
        int data = 0;
        // 1.将数据从blockqueue中获取--获取数据
        bq->pop(&data);
        // 2.结合某种业务逻辑,处理数据
        std::cout << "consumer data: " << data << std::endl;
    }
}
void *productor(void *args)
{
    BlockQueue<int> *bq = (BlockQueue<int> *)args;
    while (true)
    {
        
        // 1.先通过某种渠道获取数据
        int data = rand() % 10 + 1;
        // 2.将数据推送到blockqueue--完成生产过程
        bq->push(data);
        std::cout << "productor data: " << data << std::endl;
    }
}

int main()
{
    srand((uint64_t)time(nullptr) * getpid());
    BlockQueue<int>* bq = new BlockQueue<int>();

    //单生成单消费
    pthread_t c, p;
    pthread_create(&c, nullptr, consumer, bq);
    pthread_create(&p, nullptr, productor, bq);

    pthread_join(c, nullptr);
    pthread_join(p, nullptr);

    //多生产多消费也可以但是这个代码有问题打印输出很奇怪
    //pthread_t c[2], p[2];
    // pthread_create(&c[0], nullptr, consumer, bq);
    // pthread_create(&c[1], nullptr, consumer, bq);

    // pthread_create(&p[0], nullptr, productor, bq);
    // pthread_create(&c[1], nullptr, consumer, bq);


    // pthread_join(c[0], nullptr);
    // pthread_join(c[1], nullptr);

    // pthread_join(p[0], nullptr);
    // pthread_join(p[1], nullptr);

    return 0;
}

最后输出:
在这里插入图片描述

记忆生产消费模型 321原则
三种关系生产者和生产者 互斥关系(生产数据进入交易场所,需要加锁理解交易场所时共享资源因此互斥);消费者和消费者 互斥关系(从交易场所拿出数据同理);生产者和消费者即使互斥关系又是同步关系(互斥关系同理,同步关系则需要理解为何等待为何唤醒)

两种角色:生产者 消费者

一个交易场所:通常是缓冲区

生产者消费者模型优点

高效(支持并发)
解耦(生产者和消费者并不会直接关联)
支持忙闲不均

生产消费模型效率高体现在哪里?

不要认为生产和消费就是数据从一个生产者放入缓冲区然后消费者再从交易场所中拿数据
重点考虑生产者所需的数据从哪里来和消费者如何对数据进行处理
当消费者在对数据进行处理时候,生产者此时既可能在把数据不断的生产进交易场所也可能在接收各各地方传来的数据,而当消费者在从交易场所拿数据时候,生产者也可以在进行接收各各地方传来的数据,因为交易场所中可能已经就有缓存好的数据,都不影响消费者的操作,都是并行运行

而同理多生产多消费,同一个交易场所生产进数据和消费出数据必然是互斥的串行的,但是生产消费之所以高效 理解:当一个消费者在从交易场所拿取数据时候,另外的剩余的多个消费者可能都在处理数据,所有的生产者也可以都在进行数据的接收任务,一个拿另外处理所有接收都处于并行。

信号量

基于环形队列的生产消费模型
例子:认识信号量接口并且理解环形队列的生产消费模型

main.cc

#include "ringQueue.hpp"
#include <iostream>
#include <pthread.h>
#include <sys/types.h>
#include <unistd.h>

void* consumerRoutine(void*args)
{
    ringQueue<int>*rq = (ringQueue<int>*)args;
    while(true)
    {
        sleep(1);
        int data=0;
        rq->pop(&data);
        std::cout<<"consumer done ->"<<data<<std::endl;
    }
} 
void* productorRoutine(void*args)
{
    ringQueue<int>*rq = (ringQueue<int>*)args;
    while(true)
    {
        
        int data = rand()%10+1;
        rq->push(data);
        std::cout<<"productor done ->"<<data<<std::endl;
    }
}

int main()
{
    srand(time(nullptr)*getpid());//种下随机数的种子
    ringQueue<int>*rq = new ringQueue<int>();

    pthread_t c,p;
    pthread_create(&c,nullptr,consumerRoutine,rq);//rq传进去让线程都能看到
    pthread_create(&p,nullptr,productorRoutine,rq);

    pthread_join(c,nullptr);
    pthread_join(p,nullptr);

    return 0;
}

ringQueue.hpp

#include<iostream>
#include<vector>
#include <semaphore.h>

const int num= 5;

template<class T>
class ringQueue
{
public:
    ringQueue(const int cap =num ):_cap(cap),_ring(cap)
    {
        sem_init(&_sem_data,0,0);  //对信号量初始化 最后一个参数就代表该信号量的初始大小理解信号量即一个计数器
        sem_init(&_sem_space,0,_cap);
        _cStep=_pStep=0;//下标初始化
    }
    void push(const T in)
    {
        //生产者 p操作 --
        sem_wait(&_sem_space);
        //生产者关心空间,当空间信号量还有资源时候即不为0,线程就能够往下执行进行对环形队列数据插入
        //wait也很生动 如果没有空间信号量即0,那么线程就被休眠,等到空间信号量有,再被唤醒继续执行这里也体现了同步互斥的概念

        _ring[_pStep++]=in;
        _pStep%=num;

        //生产者 v操作
        sem_post(&_sem_data);
    }

    void pop(T*out)
    {
    	//消费者 p操作 --
        sem_wait(&_sem_data);

        *out=_ring[_cStep++];
        _cStep%=num;
		
		//消费者 v操作 ++
        sem_post(&_sem_space);
    }

    ~ringQueue()
    {
        sem_destroy(&_sem_data);
        sem_destroy(&_sem_space);
    }
private:
    std::vector<T> _ring;
    int _cap;

    int _cStep;//消费者下一次拿出数据的下标
    int _pStep;//生产者下一次放进数据的下标

    //对信号量的本质理解:就是一个记录有多少资源数量的计数器
    //信号量的一些列函数都需要 &sem 本质就是pv操作信号量的值
    sem_t _sem_space;//生产者关心空间 当空间有时,进行p操作空间信号量-- 把数据传输 再执行v操作 数据信号量++
    sem_t _sem_data; //消费者关心数据 当数据有时,进行p操作数据信号量-- 把数据拿出 在执行v操作 空间信号量++
};

输出内容
在这里插入图片描述
环形队列先全部被生产者放满数据导致信号量_sem_space为0,因此线程p被阻塞,等待消费者消费数据释放信号量_sem_space,一次往后都是消费者消费一个,生产者才能生产一个。

信号量的一系列接口:头文件#include <semaphore.h>
首先定义信号量对象

初始化信号量:
int sem_init(sem_t *sem, int pshared, unsigned int value);
参数:
pshared:0表示线程间共享,非零表示进程间共享
value:信号量初始值

等待信号量:p操作
功能:等待信号量,会将信号量的值减1
int sem_wait(sem_t *sem); //P()

发布信号量:v操作
功能:发布信号量,表示资源使用完毕,可以归还资源了。将信号量值加1。
int sem_post(sem_t *sem);//V()

销毁信号量:
int sem_destroy(sem_t *sem);

信号量本质就是一个计数器,可以看到上述代码的同步互斥实现和使用条件变量对象相比,信号量就显得很方便,那么理解何时使用信号量何时使用条件变量,当交易场所被当作一个整体时候用条件变量实现同步(进数据和出数据是互斥的),而当交易场所能够被分段使用列入上述的交易场所是个vector对象,则可以使用信号量(进数据和出数据大部分情况可以并发执行互不影响)

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值