秒懂Linux之线程(中)

fe594ea5bf754ddbb223a54d8fb1e7bc.gif

目录

 

前言

多线程场景

普遍情况

类对象多线程

C11场景下的多线程

线程管理

线程的模拟封装

线程分离

线程互斥

demo(线程抢票)

初始化互斥量

销毁互斥量

互斥量加锁与解锁

改进demo

互斥量实现原理探究

线程同步 

条件变量

初始化条件变量

销毁条件变量

条件变量等待与唤醒

demo(验证线程同步机制)

初步准备:

实现同步:

生产消费模型

生产消费模拟实现

初步准备:

BlockingQueue:

生产消费plus

全部代码


前言

在前面的文章中我们主要接触单线程下的场景,那么现在我们来尝试一下多线程等等更多的新知识点~秒懂Linux之线程-CSDN博客

多线程场景

普遍情况

//创建5个线程
const int thread_num = 5;



void *handlerTask(void* args)
{
    const char* thread_name = (char*)args;
    while(true)
    {
        sleep(1);
        cout<<"I am "<<thread_name<<endl;
    }

    delete [] thread_name;

    return nullptr;
}

//多线程创建
int main()
{
    vector<pthread_t> threads;
    for(int i = 0;i<thread_num;i++)
    {
        //线程命名缓冲区
        char *thread_name = new char[64];
        //char thread_name[64];  错误命名方法,因为线程是共享大部分资源的也包括缓冲区

        //对每个线程进行命名
        snprintf(thread_name,64,"Thread-%d",i+1);

        //snprintf(thread_name,sizeof(thread_name),"Thread-%d",i+1);
        pthread_t tid;
        //把thread_name(缓冲区地址而非字符串)上传到线程执行任务的handlerTask函数中
        pthread_create(&tid,nullptr,handlerTask,thread_name);
        sleep(1);
        //把每个线程的tid收集起来以便控制
        threads.push_back(tid);
    }

    //回收每个线程
    for(auto &tid:threads)
    {
        pthread_join(tid,nullptr);
    }

    return 0;
}

多线程创建成功~

线程传参和返回值,我们可以传递级别信息,也可以传递其他对象~

类对象多线程

//创建5个线程
const int thread_num = 5;


class Task
{
    public:
    Task()
    {}
    void SetData(int x,int y)
    {
        datax = x;
        datay =y;
    }
    int Excute()
    {
        return datax+datay;
    }
    ~Task()
    {}
    private:
    int datax;
    int datay;
};

class ThreadData : public Task
{
    public:
    ThreadData(int x,int y,const string& thread_name):_thread_name(thread_name)
    {
        _t.SetData(x,y);
    }
    string thread_name()
    {
        return _thread_name;
    }
    int run ()
    {
        return _t.Excute();
    }
    private:
    string _thread_name;
    Task _t;
};

class Result
{
    public:
    Result()
    {}
    ~Result()
    {}
    void SetResult(int result,const string &thread_name)
    {
        _result = result;
        _thread_name = thread_name;
    }
    void Print()
    {
        cout << _thread_name << " : " << _result << endl;
    }
    private:
    int _result;
    string _thread_name;
};

void *handlerTask(void* args)
{
    ThreadData* td = (ThreadData*)args;
    string name = td->thread_name();

    Result *res = new Result();
    int result = td->run();
    res->SetResult(result,name);

    cout<<name<<"run result"<<result<<endl;
    delete td;

    sleep(1);
    return res;
   
}

//多线程创建
int main()
{
    vector<pthread_t> threads;
    for(int i = 0;i<thread_num;i++)
    {
        //线程命名缓冲区
        char thread_name[64];
        //对每个线程进行命名
        snprintf(thread_name,64,"Thread-%d",i+1);
        ThreadData* td = new ThreadData(10,20,thread_name);
        pthread_t tid;
        pthread_create(&tid,nullptr,handlerTask,td);
        sleep(1);
        threads.push_back(tid);
    }

    vector<Result*> result_set;
    void *ret = nullptr;
    for (auto &tid : threads)
    {
        pthread_join(tid, &ret);
        result_set.push_back((Result*)ret);
    }

    for(auto & res : result_set)
    {
        res->Print();
        delete res;
    }

    return 0;
}

了解即可,毕竟有点绕~

C11场景下的多线程


void* threadrun(int num)
{
    while(num)
    {
        cout<<"I am a thread,num:"<<num<<endl;
        sleep(1);
    }
    return nullptr;
}

//C11下线程
int main()
{
    thread t1(threadrun,10);
    thread t2(threadrun,10);
    thread t3(threadrun,10);
    thread t4(threadrun,10);
    while(true)
    {
        cout<<"I am a main thread"<<endl;
        sleep(1);
    }
    t1.join();
    t2.join();
    t3.join();
    t4.join();

    return 0;
}

C++11的多线程,是对原生线程的封装~  无论是怎样最终都是用户通过封装好的接口来进行线程创建,线程等待等操作~(不过还是建议用系统调用)

线程管理

既然我们对线程进程诸多控制,那么线程又该如何管理呢?先描述,再组织~

pthread作为动态库也被称为共享库,当映射到地址空间后正文代码遇到相关函数就可以到共享区寻找对应方法完成一次库函数调用~

对于线程库而言,自然是要来管理多个线程的。对线程的描述划分为3部分:

首先线程tcb的起始地址就是线程的tid~

接着,每个线程也是有自己的独立栈的,这样才能保证在共享大部分资源的同时保证自己内部数据的独立性。

最后对于线程局部存储而言,可以在全局变量前加入__thread让线程都私有一份全局变量(唯内置类型)。

线程的模拟封装

#ifndef __THREAD_HPP__
#define __THREAD_HPP__

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

namespace MyThread
{
    template <class T>
    using func_t = std::function<void(T&)>;
    // typedef std::function<void(const T&)> func_t;

    template <class T>
    class Thread
    {
    public:
        void excute()
        {
            _func(_data);
        }
    public:
        Thread(func_t<T> func, const T& data,const std::string &name = "none-name")
            : _func(func), _data(data), _threadname(name), _stop(true)
        {}
        
        static void* threadrun(void* rags)//变为静态函数,不再有隐藏this
        {   

            //执行回调方法&&传递_data给该方法
            //不过类内的成员对象要访问是需要带this指针的~ 所以我们间接来实现~
            Thread<T>* self = (Thread<T>*) rags;
            self->excute();
            //_func(_data);


            //但是让线程跑这个函数是没有意义的,我们想要的是让线程跑——func函数~
            /* while(true)
            {
                std::cout<<"I am a thread"<<endl;
                sleep(1);
            } */
            return nullptr;
        }

        bool start()
        {
            //这里需要注意一点,在类中的成员函数是有隐藏this指针的,如果用Nullptr传递的话参数个数会不匹配
            //int n = pthread_create(&tid,nullptr,threadrun,nullptr)
            //传递this指针让rags接收
            int n = pthread_create(&_tid,nullptr,threadrun,this);
            if(!n)
            {
                _stop = false;
                return true;
            }
            else{
                return false;
            }
        }

        void detach()
        {
            if(!_stop)
            {
                pthread_detach(_tid);
            }
        }

        void join()
        {
            if(!_stop)
            {
                pthread_join(_tid,nullptr);
            }
        }

        std::string name()
        {
            return _threadname;
        }

        void stop()
        {
            _stop = true;
        }

        ~Thread()
        {}

    private:
        pthread_t _tid;
        std::string _threadname;
        T _data;  // 为了让所有的线程访问同一个全局变量
        func_t<T> _func;
        bool _stop;
    };

}

#endif
#include <iostream>
#include <vector>
#include "Thread.hpp"
using namespace MyThread;

//我们要线程执行的任务
void runfunc(int & cnt)
{
    while(cnt)
    {
        std::cout<<"I am new thread, cnt:"<<cnt<<std::endl;
        cnt--;
        sleep(1);
    }
}
const int num = 4;
int main()
{
    std::vector<Thread<int>> threads;
    //1.创建一批线程
    for(int i = 0;i<num;i++)
    {
        std::string name = "thread-"+std::to_string(i+1);
        threads.emplace_back(runfunc,3,name);
    }

    //2.启动一批进程
    for(auto& e:threads)
    {
        e.start();
    }

    //3.等待一批进程
    for(auto&e:threads)
    {
        e.join();
        std::cout<<"wait thread done, name: "<<e.name()<<std::endl;
    }
}

线程分离

我们目前的等待都是阻塞等待,主线程得等新线程回收完毕拿到结果才可以去执行下一步操作~

void* threadrun(void* rags)
{
    string name = (char*)rags;
    while(true)
    {
        cout<<"I am a new thread"<<endl;
        sleep(1);
    }
    return nullptr;
}

int main()
{
    pthread_t tid;
    pthread_create(&tid,nullptr,threadrun,(void*)"thread 1");

    cout<<"main thread block"<<endl;
    pthread_join(tid,nullptr);
    cout<<"main wait sucess"<<endl;

    return 0;
}

而我们的线程分离就是让阻塞等待变成非阻塞等待,让主线程不再关心新线程结果——线程分离后由线程自行回收,不需要join主动回收——仅此而已~ 

void* threadrun(void* rags)
{
    pthread_detach(pthread_self());
    string name = (char*)rags;
    while(true)
    {
        cout<<"I am a new thread"<<endl;
        sleep(1);
    }
    return nullptr;
}

int main()
{
    pthread_t tid;
    pthread_create(&tid,nullptr,threadrun,(void*)"thread 1");
    //主线程也可以控制新线程的分离
    //pthread_detach(tid);

    cout<<"main thread block"<<endl;
    //pthread_join(tid,nullptr); 线程分离后不能再去回收它,否则会出错
    cout<<"main wait sucess"<<endl;

    return 0;
}

最终分离的线程无论是线程异常还是主线程先退出进而进程退出都是遵循该规律的,分离仅仅是自动回收的功效~ 

线程互斥

demo(线程抢票)

namespace MyThread
{
    template <class T>
    using func_t = std::function<void(T)>;
    // typedef std::function<void(const T&)> func_t;

    template <class T>
    class Thread
    {
    public:
        void excute()
        {
            _func(_data);
        }
    public:
        Thread(func_t<T> func,  T data,const std::string &name = "none-name")
            : _func(func), _data(data), _threadname(name), _stop(true)
        {}
        
        static void* threadrun(void* rags)//变为静态函数,不再有隐藏this
        {   

            //执行回调方法&&传递_data给该方法
            //不过类内的成员对象要访问是需要带this指针的~ 所以我们间接来实现~
            Thread<T>* self = (Thread<T>*) rags;
            self->excute();
            //_func(_data);


            //但是让线程跑这个函数是没有意义的,我们想要的是让线程跑——func函数~
            /* while(true)
            {
                std::cout<<"I am a thread"<<endl;
                sleep(1);
            } */
            return nullptr;
        }

        bool start()
        {
            //这里需要注意一点,在类中的成员函数是有隐藏this指针的,如果用Nullptr传递的话参数个数会不匹配
            //int n = pthread_create(&tid,nullptr,threadrun,nullptr)
            //传递this指针让rags接收
            int n = pthread_create(&_tid,nullptr,threadrun,this);
            if(!n)
            {
                _stop = false;
                return true;
            }
            else{
                return false;
            }
        }

        void detach()
        {
            if(!_stop)
            {
                pthread_detach(_tid);
            }
        }

        void join()
        {
            if(!_stop)
            {
                pthread_join(_tid,nullptr);
            }
        }

        std::string name()
        {
            return _threadname;
        }

        void stop()
        {
            _stop = true;
        }

        ~Thread()
        {}

    private:
        pthread_t _tid;
        std::string _threadname;
        T _data;  // 为了让所有的线程访问同一个全局变量
        func_t<T> _func;
        bool _stop;
    };

}
// demo——多线程抢票
int g_tickets = 10000;

class threadData
{
public:
    threadData(int &tickets, const std::string &name)
        : _tickets(tickets), _name(name), _total(0)
    {}
    ~threadData()
    {}

public:
    int &_tickets; // 所有的线程,最后都会引用同一个全局的g_tickets
    std::string _name;
    int _total;
};

void ticketrun(threadData *td)
{
    while (true)
    {
        if (td->_tickets > 0)
        {
            usleep(1000);
            printf("%s running, get tickets: %d\n", td->_name.c_str(), td->_tickets); 
            td->_tickets--;
            td->_total++;
        }
        else
        {
            break;
        }
    }
}


const int num = 4;
int main()
{
    //;std::vector<Thread<int>> threads;
    std::vector<Thread<threadData*>> threads;

    std::vector<threadData *> datas;

    // 1.创建一批线程
    for (int i = 0; i < num; i++)
    {
        std::string name = "thread-" + std::to_string(i + 1);
        threadData *td = new threadData(g_tickets, name);
        threads.emplace_back(ticketrun, td, name);
        datas.emplace_back(td);
    }

    // 2.启动一批进程
    for (auto &e : threads)
    {
        e.start();
    }

    // 3.等待一批进程
    for (auto &e : threads)
    {
        e.join();
        std::cout << "wait thread done, name: " << e.name() << std::endl;
    }
    sleep(1);
    // 4. 输出统计数据
    for (auto data : datas)
    {
        std::cout << data->_name << " : " << data->_total << std::endl;
        delete data;
    }
}

目前我们发现一个问题:为什么能抢到负数的票呢?

共享资源(g_tickets) 在被访问时没有被保护,并且本身操作是没有原子的~

要解决以上问题,需要做到三点:
  • 代码必须要有互斥行为:当代码进入临界区执行时,不允许其他线程进入该临界区。
  • 如果多个线程同时要求执行临界区的代码,并且临界区没有线程在执行,那么只能允许一个线程进入该临界区。
  • 如果线程不在临界区中执行,那么该线程不能阻止其他线程进入临界区。
要做到这三点,本质上就是需要一把锁。Linux上提供的这把锁叫互斥量。

初始化互斥量

静态分配:
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER
动态分配:
int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict
attr);
参数:
mutex :要初始化的互斥量
attr NULL

销毁互斥量

int pthread_mutex_destroy(pthread_mutex_t *mutex)

  • 使用 PTHREAD_ MUTEX_ INITIALIZER 初始化的互斥量不需要销毁
  • 不要销毁一个已经加锁的互斥量
  • 已经销毁的互斥量,要确保后面不会有线程再尝试加锁

互斥量加锁与解锁

int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
返回值 : 成功返回 0, 失败返回错误号

调用 pthread_ lock 时,可能会遇到以下情况:

改进demo

出现的并发访问的问题,本质是因为多个执行流执行访问全局数据的代码导致的~

而保护全局资源本质就是通过保护临界区完成的~

静态分配:

pthread_mutex_t g_mutex = PTHREAD_MUTEX_INITIALIZER;
// demo——多线程抢票
int g_tickets = 10000;

void ticketrun(threadData *td)
{
    while (true)
    {
        //访问临界资源(共享资源)的代码,叫做临界区——对其加锁本质就是把并发执行改为串行执行
        pthread_mutex_lock(&g_mutex);//加锁
        if (td->_tickets > 0)//1
        {
            usleep(1000);
            printf("%s running, get tickets: %d\n", td->_name.c_str(), td->_tickets); //2
            td->_tickets--;//3
            pthread_mutex_unlock(&g_mutex);//解锁
            td->_total++;
        }
        else
        {
            pthread_mutex_unlock(&g_mutex);//解锁
            break;
        }
    }
}

动态分配: 

class threadData
{
public:
    threadData(int &tickets, const std::string &name,pthread_mutex_t &mutex)
        : _tickets(tickets), _name(name), _total(0),_mutex(mutex)
    {}
    ~threadData()
    {}

public:
    int &_tickets; // 所有的线程,最后都会引用同一个全局的g_tickets
    std::string _name;
    int _total;
    pthread_mutex_t &_mutex;//全部线程共用一把锁
};

//pthread_mutex_t g_mutex = PTHREAD_MUTEX_INITIALIZER;
// demo——多线程抢票
int g_tickets = 10000;

void ticketrun(threadData *td)
{
    while (true)
    {
        //访问临界资源(共享资源)的代码,叫做临界区——对其加锁本质就是把并发执行改为串行执行
        //pthread_mutex_lock(&g_mutex);//加锁 :锁是自由竞争的,若是某个线程竞争能力过强会导致饥饿问题
        pthread_mutex_lock(&td->_mutex);
        if (td->_tickets > 0)//1
        {
            usleep(1000);
            printf("%s running, get tickets: %d\n", td->_name.c_str(), td->_tickets); //2
            td->_tickets--;//3
            pthread_mutex_unlock(&td->_mutex);//解锁
            td->_total++;
        }
        else
        {
            pthread_mutex_unlock(&td->_mutex);//解锁
            break;
        }
    }
}
const int num = 4;
int main()
{
    //动态分配
    pthread_mutex_t mutex;
    pthread_mutex_init(&mutex,nullptr);
    //;std::vector<Thread<int>> threads;
    std::vector<Thread<threadData*>> threads;

    std::vector<threadData *> datas;

    // 1.创建一批线程
    for (int i = 0; i < num; i++)
    {
        std::string name = "thread-" + std::to_string(i + 1);
        threadData *td = new threadData(g_tickets, name,mutex);
        threads.emplace_back(ticketrun, td, name);
        datas.emplace_back(td);
    }

    // 2.启动一批进程
    for (auto &e : threads)
    {
        e.start();
    }

    // 3.等待一批进程
    for (auto &e : threads)
    {
        e.join();
        std::cout << "wait thread done, name: " << e.name() << std::endl;
    }
    sleep(1);
    // 4. 输出统计数据
    for (auto data : datas)
    {
        std::cout << data->_name << " : " << data->_total << std::endl;
        delete data;
    }
}

小优化:

class LockGuard
{
public:
    LockGuard(pthread_mutex_t *mutex):_mutex(mutex)
    {
        pthread_mutex_lock(_mutex); // 构造加锁
    }
    ~LockGuard()
    {
        pthread_mutex_unlock(_mutex);
    }
private:
    pthread_mutex_t *_mutex;
};
void ticketrun(threadData *td)
{
    while (true)
    {
        //访问临界资源(共享资源)的代码,叫做临界区——对其加锁本质就是把并发执行改为串行执行
        //pthread_mutex_lock(&g_mutex);//加锁 :锁是自由竞争的,若是某个线程竞争能力过强会导致饥饿问题
        //pthread_mutex_lock(&td->_mutex);

        LockGuard guard(&td->_mutex); // 临时对象, RAII风格的加锁和解锁
        if (td->_tickets > 0)//1
        {
            usleep(1000);
            printf("%s running, get tickets: %d\n", td->_name.c_str(), td->_tickets); //2
            td->_tickets--;//3
            td->_total++;
        }
        else
        {
            break;
        }
    }
    //离开作用域自动解锁
}

互斥量实现原理探究

  • 经过上面的例子,大家已经意识到单纯的 i++ 或者 ++i 都不是原子的,有可能会有数据一致性问题
  • 为了实现互斥锁操作,大多数体系结构都提供了swapexchange指令,该指令的作用是把寄存器和内存单元的数据相交换,由于只有一条指令,保证了原子性

CPU寄存器硬件只有一套,但是CPU寄存器内部的数据,数据线程的硬件上下文是有很多份的~

数据在内存里,所有线程都能访问,属于共享的~但是转移到CPU内部寄存器中,就属于一个线程私有了!

交换的本质不是拷贝到寄存器(交换的时候只有一条汇编,是原子的),而且所有线程在竞争锁的时候只有一个1~

线程同步 

条件变量

下面我们再来讨论一下关于竞争锁存在的饥饿问题~

条件变量存在本质:解决线程竞争锁导致的饥饿问题~ 

初始化条件变量

动态:

int pthread_cond_init(pthread_cond_t *restrict cond,const pthread_condattr_t *restrict
attr);
参数:
cond :要初始化的条件变量
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_broadcast(pthread_cond_t *cond);
int pthread_cond_signal(pthread_cond_t *cond);

demo(验证线程同步机制)

从上面的抢票demo中我们可以看出只要加了锁必定存在竞争,要想解决竞争就必须要加入条件变量让它排队,避免锁一直在某个线程上~wait——>排队,singal——>铃铛唤醒~

demo:利用cond让一个线程控制其他线程

初步准备:

// 核心线程执行处
void *MasterRun(void *args)
{
    std::string name = (char *)args;
    while (true)
    {
        std::cout << name << std::endl;
        sleep(1);
    }
}

// 其他线程执行处
void* SlaverRun(void* args)
{
    std::string name = (char *)args;
    while(true)
    {
        std::cout<<name<<std::endl;
        sleep(1);
    }
}

void StartMaster(std::vector<pthread_t> *ptids)
{
    // 创建核心线程
    pthread_t tid;
    int n = pthread_create(&tid, nullptr, MasterRun, (void *)"Master Thread");
    if (n == 0)
    {
        std::cout << "create master success" << std::endl;
    }
    ptids->emplace_back(tid);
}


void StartSlaver(std::vector<pthread_t> *ptids, int threadnum = 3)
{
    // 创建多线程
    for (int i = 0; i < threadnum; i++)
    {
        char *name = new char[64];
        // 批量命名线程
        snprintf(name, 64, "slaver-%d", i + 1);
        pthread_t tid;
        int n = pthread_create(&tid, nullptr, SlaverRun, name);
        if (n == 0)
        {
            std::cout << "create Slaver success: " << name << std::endl;
            ptids->emplace_back(tid);
        }
    }
}

void WaitThread(std::vector<pthread_t> &tids)
{
    for(auto &e:tids)
    {
        pthread_join(e,nullptr);
    }
}

int main()
{
    // 记录tid后续用来回收
    std::vector<pthread_t> tids;

    // 核心线程函数
    StartMaster(&tids);

    // 其他线程函数
    StartSlaver(&tids,5);

    // 回收线程函数
    WaitThread(tids);

    return 0;
}

要想使用条件变量就得先加锁~

实现同步:

// 核心线程执行处
void *MasterRun(void *args)
{
    //我们让其3s后去唤醒线程
    sleep(3);
    std::cout<<"核心线程开始进行唤醒工作"<<std::endl;
    std::string name = (char *)args;
    while (true)
    {
        // 唤醒其中一个队列首部的线程
        pthread_cond_signal(&gcond); // 充当铃铛角色
        std::cout << "成功唤醒" << std::endl;
        sleep(1);
    }
}

// 其他线程执行处
void *SlaverRun(void *args)
{
    // 这里是多线程并发访问的地方
    std::string name = (char *)args;
    while (true)
    {
        // 加锁
        pthread_mutex_lock(&gmutex);

        // 一般我们的条件变量是在加锁与解锁之间使用
        // 想象在拿着锁的情况下在临界区找苹果(临界资源),万一找不到就让它去等待
        // 等待的过程也要把锁给释放掉
        pthread_cond_wait(&gcond, &gmutex); // 充当排队角色

        std::cout << "当前被唤醒的线程是: " << name << std::endl;

        // 解锁
        pthread_mutex_unlock(&gmutex);
    }
}

本来在SlaveRun处是多线程并发执行的,然而在有了条件变量后我们通过MasterRun的铃铛与该函数内的队列达到了同步的效果——串行执行~

// 核心线程执行处
void *MasterRun(void *args)
{
    //我们让其3s后去唤醒线程
    sleep(3);
    std::cout<<"核心线程开始进行唤醒工作"<<std::endl;
    std::string name = (char *)args;
    while (true)
    {
        // 唤醒其中一个队列首部的线程
        //pthread_cond_signal(&gcond); // 充当铃铛角色
        
        // 唤醒其中一个队列所有的线程
        pthread_cond_broadcast(&gcond); 
        std::cout << "成功唤醒" << std::endl;
        sleep(1);
    }
}

demo验证成功:说明条件变量确实是可以达到同步机制的~

生产消费模型

怎么说呢,这里的生产者就相当于前面的p2(放苹果的人),消费者就相当于p1(拿苹果的人)。互斥的意义在于实现数据的原子性,避免出现错误,而同步的意义在于避免因为互斥出现的竞争问题。

生产消费模拟实现

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

在BlockingQueue中其实就是对临界资源的一种限制,让生产者无法在临界区放更多苹果~

初步准备:

int apple = 10;

//生产者生产
void PdRun(int& data)
{
    while(data)
    {
        std::cout<<"Productor :"<<data--<<std::endl;
        sleep(1);
    }
}

//消费者消费
void CsRun(int& data)
{
    while(data)
    {
        std::cout<<"Consumer :"<<data--<<std::endl;
        sleep(1);
    }
}

void StartProductor(std::vector<Thread<int>> *threads,int num)
{
    for(int i =0;i<num;i++)
    {
        std::string name = "thread-"+std::to_string(i+1);
        threads->emplace_back(PdRun,apple,name);
        //启动(创建)线程
        threads->back().start();
    }
}

void StartConsumer(std::vector<Thread<int>> *threads,int num)
{
    for(int i =0;i<num;i++)
    {
        std::string name = "thread-"+std::to_string(i+1);
        threads->emplace_back(CsRun,apple,name);
        //启动(创建)线程
        threads->back().start();
    }
}

void WaitThread(std::vector<Thread<int>> &threads)
{
    for (auto &e : threads)
    {
        e.join();
    }
}

int main()
{
    //本质上的共享资源是苹果(int)
    //采用模拟包装的线程:Thread
    std::vector<Thread<int>> threads;
    //生产者函数
    StartProductor(&threads,1);
    //消费者函数
    StartConsumer(&threads,1);
    //回收函数
    WaitThread(threads);

    return 0;
}

BlockingQueue:


// BlockQueue——存储苹果(临界资源)的一种数据结构
template <class T>
class BlockQueue
{
private:
    //针对生产者判断队列是否已满
    bool IsFull()
    {
        return _block_queue.size() == _cap;
    }
    //针对消费者判断队列是否为空
    bool IsEmpty()
    {
        return _block_queue.empty();
    }

public:
    BlockQueue(int cap)
        : _cap(cap)
    {
        pthread_mutex_init(&_mutex, nullptr);
        pthread_cond_init(&_product_cond, nullptr);
        pthread_cond_init(&_consum_cond, nullptr);
    }

    // 生产者专用接口:入队列
    void Enqueue(const T &apple)
    {
        //加锁
        pthread_mutex_lock(&_mutex);

        while(IsFull())
        {
            //若已满,生产线程去等待
            pthread_cond_wait(&_product_cond,&_mutex);//1.排队等待 2.释放曾拥有的锁
        }
        //进行生产
        _block_queue.push(apple);

        //通知消费者来消费(铃铛)
        pthread_cond_signal(&_consum_cond);

        //解锁
        pthread_mutex_unlock(&_mutex);
    }

    // 消费者专用接口:出队列
    void Pop(T *apple)
    {
        //加锁
        pthread_mutex_lock(&_mutex);

        while(IsEmpty())
        {
            //若为空,消费线程去等待
            pthread_cond_wait(&_consum_cond,&_mutex);
        }
        //进行消费
        //记录所消费过的数据
        *apple = _block_queue.front();
        _block_queue.pop();

        //通知生产者来生产(铃铛)
        pthread_cond_signal(&_product_cond);

        //解锁
        pthread_mutex_unlock(&_mutex);
    }
    ~BlockQueue()
    {
        pthread_mutex_destroy(&_mutex);
        pthread_cond_destroy(&_product_cond);
        pthread_cond_destroy(&_consum_cond);
    }

private:
    std::queue<T> _block_queue;   // 阻塞队列,是被整体使用的
    int _cap;                     // 最大容量
    pthread_mutex_t _mutex;       // 保护_block_queue的锁
    pthread_cond_t _product_cond; // 专门给生产者提供的条件变量
    pthread_cond_t _consum_cond;  // 专门给消费者提供的条件变量
};

#endif
int apple = 10;

//生产者生产
void PdRun(BlockQueue<int> &bq)
{   
    int cnt =1;
    while(true)
    {
        bq.Enqueue(cnt);
        std::cout << "Productor product data is : " << cnt << std::endl;
        //一直生产,产品名为cnt
        cnt++;
        sleep(1); 
    }
}

//消费者消费
void CsRun(BlockQueue<int> &bq)
{
    while(true)
    {
        int data;
        bq.Pop(&data);//拿到消费的历史数据
        std::cout << "Consumer Consum data is : " << data <<  std::endl;
        sleep(1);
    }
}

//代码复用
void StartComm(std::vector<Thread<BlockQueue<int>>> *threads, int num, BlockQueue<int> &bq, func_t<BlockQueue<int>> func)
{
    for (int i = 0; i < num; i++)
    {
        std::string name = "thread-" + std::to_string(i + 1);
        threads->emplace_back(func, bq, name);
        threads->back().start();
    }
}

void StartProductor(std::vector<Thread<BlockQueue<int>>> *threads, int num, BlockQueue<int> &bq)
{
    StartComm(threads, num, bq, PdRun);
}

void StartConsumer(std::vector<Thread<BlockQueue<int>>> *threads, int num, BlockQueue<int> &bq)
{
    StartComm(threads, num, bq, CsRun);
}

void WaitThread(std::vector<Thread<BlockQueue<int>>> &threads)
{
    for (auto &e : threads)
    {
        e.join();
    }
}

int main()
{
    //本质上的共享资源是苹果(int)
    //用BlockingQueue来包装苹果——最多存放数据5个
    BlockQueue<int> *bq = new BlockQueue<int>(5);
    //采用模拟包装的线程:Thread
    std::vector<Thread<BlockQueue<int>>> threads;
    //std::vector<Thread<int>> threads;
    //生产者函数
    StartProductor(&threads,1,*bq);
    //消费者函数
    StartConsumer(&threads,1,*bq);
    //回收函数
    WaitThread(threads);

    return 0;
}

我们成功利用BlockingQueue实现了生产消费模型:生产者在队列里生产数据,消费者从队列里拿走数据。

下面我们再来谈谈里面的细节问题:为什么这里用的是while而不是if呢?

因为wait之后终究是会被唤醒的,而唤醒之后的线程就会继续向下执行。假设现在有1个生产者与5个消费者,当生产者生产1个数据后把5个消费者全部唤醒ps:在临界区被唤醒为无锁状态。第一个消费者竞争到锁并消费完毕,那另外4个消费者在干嘛呢?——等待锁的释放并重新竞争(竞争成功才可以返回),若我们的判断是if,那么剩下竞争成功的线程就会跳出判断去强制消费,可产品已经没了,所以这是一个大问题,也是为什么我们要用while的原因~

BlockingQueue不仅仅可以交易整型还可以是任务~

生产消费plus

void PdRun(BlockQueue<Task> &bq)
{   
    srand(time(nullptr)^pthread_self());
    while(true)
    {
        //1.获取任务
        int a = rand()%10+1;
        usleep(1000);
        int b = rand()%20+1;
        Task t(a,b);
        //2.把获取的任务放到队列中
        bq.Enqueue(t);
        std::cout << "Productor product data is : " << t.DebugToString() << std::endl;
        sleep(1); 
    }
}

void CsRun(BlockQueue<Task> &bq)
{
    sleep(1);
    while(true)
    {
        //1.从队列中取下任务
        Task t;
        bq.Pop(&t);
        //2.处理任务
        t.Excute();
        std::cout << "Consumer Consum data is : " << t.ResultToString() <<  std::endl;
        sleep(1);
    }
}
class Task
{
public:
    Task() {}
    Task(int a, int b) : _a(a), _b(b), _result(0)
    {
    }
    void Excute()
    {
        _result = _a + _b;
    }
    std::string ResultToString()
    {
        return std::to_string(_a) + "+" + std::to_string(_b) + "=" + std::to_string(_result);
    }
    std::string DebugToString()
    {
        return std::to_string(_a) + "+" + std::to_string(_b) + "=?";
    }

private:
    int _a;
    int _b;
    int _result;
};

全部代码

线程上 · 14aecaf · 玛丽亚后/keep - Gitee.com

线程互斥 · 9c9cf54 · 玛丽亚后/keep - Gitee.com

demo:验证条件变量同步机制 · d8fd392 · 玛丽亚后/keep - Gitee.com

生产消费模型 · c23df76 · 玛丽亚后/keep - Gitee.com

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值