Linux--多线程(4)

1. 线程池

2. 可处理任务的线程池模型

这个模型类似于基于阻塞队列的线程池模型:点击查看博客详情

为什么说他们相似呢,因为它们都是通过队列来作缓冲区,并且都有生产者和消费者。

不同的是,这个模型的线程不是由自己一个个创建,而是由线程池创建,在线程池初始化的时候,就生产出一批线程。

当你开始生产数据的时候,就会有一部分线程被调用过去生产数据,一部分线程会被调去领取任务。

构建一个线程池,线程池里有一个队列,该队列用来接收任务。池内的线程不断生产任务,池外的线程不断领取任务。

也就是外部的请求来了,线程池内早早就准备好了线程,用来随时处理任务。

  • 初始化线程池

生产出一批线程,没有任务的时候就在挂起等待

void pthreadPoolInit()
{
    //每次开始前,让一堆线程去执行rountine
    //或者在条件变量cond_下等待(没任务)
    pthread_t tid;
    for (int i = 0; i < num_; i++)
    {
        pthread_create(&tid, nullptr, rountine, (void *)this);
    }
}

等待的原理和阻塞队列的模型一样,通过条件变量:

void wait()
{
    pthread_cond_wait(&cond_,&mutex_);
}

唤醒:

void wakeUp()
{
    pthread_cond_signal(&cond_);
}

因为其他都类似,这里就不一一列举了。

有个值得注意的问题是,创建线程时需要执行的方法rountine是一个静态成员函数。因为让线程在类成员函数内直接调用成员方法,是无法实现的。 所以要将rountine设置为static方法。

还有,线程领取完任务以后,应该先释放锁再执行自己的任务。因为此时的任务已经不属于队列,而属于线程自己,所以应该先释放锁然后能让别的线程早点拿到锁。

头文件:

#pragma once
#include <queue>
#include <iostream>
#include <unistd.h>
using std::cout;
using std::endl;
namespace zcb
{
    const int g_num = 10;
    template <class T>
    class pthreadPool
    {
    private:
        int num_;
        std::queue<T> task_queue_;//临界资源
        pthread_mutex_t mutex_;
        pthread_cond_t cond_;
    public:
        pthreadPool() : num_(g_num)
        {
            pthread_mutex_init(&mutex_,nullptr);
            pthread_cond_init(&cond_,nullptr);
        }

        ~pthreadPool()
        {
            pthread_mutex_destroy(&mutex_);
            pthread_cond_destroy(&cond_);
        }

    public:
        static void *rountine(void *args)
        {
            // 每个线程分离,不用等待
            pthread_detach(pthread_self());
            pthreadPool<T> *tmp = (pthreadPool<T> *)args;
            
            //每个线程不停地竞争锁,竞争到了以后如果没有任务就挂起
            //挂起过程不带有锁
            while (1)
            {
                //访问临界资源,加锁
                tmp->lock();
                while (tmp->isEmpty())
                {
                    //如果任务队列为空,线程进行挂起
                    //只有当任务队列有任务时,才能继续
                    //如果不等待,而是直接break退出的话
                    //会导致线程一直重复访问队列是否为空,虽然没错但是效率低
                    tmp->wait();
                }
                //此时一定拿到了任务
                T t;
                cout<< "我是线程:" << pthread_self() << "我拿到了任务:"<< tmp->task_queue_.front()<<endl;
                tmp->popTask(&t);
                tmp->unlock();
                //释放完锁再去执行任务
                //因为此时的任务已经属于该进程,而不是线程池内的了
                //早点释放锁让别的进程能够及时抢锁
                sleep(1);
                cout<<"线程:"<< pthread_self()<<"任务已被执行完毕"<<endl;
            }
        }
        void pthreadPoolInit()
        {
            //每次开始前,让一堆线程去执行rountine
            //或者在条件变量cond_下等待(没任务)
            pthread_t tid;
            for (int i = 0; i < num_; i++)
            {
                pthread_create(&tid, nullptr, rountine, (void *)this);
            }
        }
        void pushTask(const T& in)
        {
            cout << "我是线程:" <<pthread_self() << "我生产任务:" << in << endl;
            lock();
            task_queue_.push(in);
            unlock();
            wakeUp();
        }
        void popTask(T* out)
        {
            //lock();
            *out = task_queue_.front();
            task_queue_.pop();
            //unlock();
        }
        void wakeUp()
        {
            pthread_cond_signal(&cond_);
        }
        void lock()
        {
            pthread_mutex_lock(&mutex_);
        }
        void unlock()
        {
            pthread_mutex_unlock(&mutex_);
        }
        bool isEmpty()
        {
            return task_queue_.size() == 0;
        }
        void wait()
        {
            pthread_cond_wait(&cond_,&mutex_);
        }
    };
}

源文件:

#include <pthread.h>
#include "pool.hpp"
using namespace zcb;

int main()
{
    pthreadPool<int>* tmp = new pthreadPool<int>();
    //初始化线程池
    tmp->pthreadPoolInit();
    srand((long long)time(nullptr));
    while(1)
    {
        sleep(1);
        int data = rand()%20 + 1;
        tmp->pushTask(data);
    }
    return 0;
}

运行结果就是在上一个线程执行完任务之前,下一个线程就已经领取到任务了:

在这里插入图片描述

2. 单例模式

某些类, 只应该具有一个对象(实例), 就称之为单例。

既然是只有一个对象,那么主要就是将类内的构造函数给设置为私有,让该类无法直接定义对象即可。

有什么作用呢?通俗的理解就是当某个类对象需要的存储空间很多,而又需要经常定义对象,就会导致每次开辟空间的时间太多。那么就可以让它只开辟一次,也即是说每次申请,用的对象都是同一个。

这个实现方式是通过一个静态成员指针变量ins和**一个静态成员方法GetInstance()**来完成的,如果该类从未创建过对象,那么该指针是nullptr,直到其第一次创建对象,就对该指针初始化,而后由于该指针不为nullptr了,每次调用GetInstance返回的都是同一个对象,这也就大大降低了每次创建对象所需要的开辟空间的时间,提高性能。

3. 饿汉方式和懒汉方式

单例模式包括饿汉模式和懒汉模式。

单例模式,也就是只定义一个对象。

那么定义一个对象,有一种情况是在你需要该对象的时候才定义,另一种是在你启动程序就直接定义好。分别是懒汉模式和饿汉模式。

饿汉模式就是在运行一段程序的时候,上来就直接创建对象。比如为设置该对象为static,在加载程序的时候直接就定义好了。如果该对象内部存在大量的空间,保存了大量的数据,或者允许发生各种拷贝,内存中就会存在冗余数据。

懒汉模式就是在运行一段程序的时候,需要了某个对象,才给程序创建出来。
比如定义一个指针,这个指针是某个对象的地址,如果该指针一直为空,说明该对象一直没被创建,只有当主动调用该封装函数去将对象new出来时,才会起到真正的定义对象的作用。这种方式的主要思想是延迟加载,能够提高程序启动的速度。

所以,要实现单例模式,首先要将构造函数私有化,然后定义一个对象指针。然后提供一个接口,用来获取对象。

单例模式实现:

头文件:

//部分代码,其他和上述模型一样
namespace zcb
{
    const int g_num = 10;
    template <class T>
    class pthreadPool
    {
    private:
        int num_;
        std::queue<T> task_queue_;//临界资源
        pthread_mutex_t mutex_;
        pthread_cond_t cond_;

        static pthreadPool<T> *ins;//新增指针,类外初始化

    private:
        //禁用拷贝构造和构造
        pthreadPool(const pthreadPool<T> &tp) = delete;
        //赋值语句
        pthreadPool<T> &operator=(pthreadPool<T> &tp) = delete;
        //构造函数必须实现并且私有化
        pthreadPool() : num_(g_num)
        {
            pthread_mutex_init(&mutex_,nullptr);
            pthread_cond_init(&cond_,nullptr);
        }
    public:
        //新增接口获得对象指针
        static ThreadPool<T> *GetInstance()
        {
            static pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
            //设置锁,防止这个函数被多线程重入
            // 当前单例对象还没有被创建
            if (ins == nullptr) //双判定,减少锁的争用,提高获取单例的效率!
            {
                pthread_mutex_lock(&lock);
                if (ins == nullptr)
                {
                    ins = new ThreadPool<T>();
                    ins->InitThreadPool();//创建对象的同时初始化线程池列表
                    std::cout << "首次加载对象" << std::endl;
                }
                pthread_mutex_unlock(&lock);
            }
            return ins;
        }
		};
}

源文件:

#include "pool_single.hpp"
#include <ctime>
#include <cstdlib>
using namespace zcb;
int main()
{
    std::cout << "当前正在运行我的进程其他代码..." << std::endl;
    sleep(5);
    srand((long long)time(nullptr));
    while(true)
    {
        sleep(1);
        int data = rand()%20+1;
        threadPool<int>::GetInstance()->pushTask(data);
        //单例本身会在任何场景,任何环境下被调用
        //GetInstance():被多线程重入,进而导致线程安全的问题,所以要设置锁
        std::cout << threadPool<Task>::GetInstance() << std::endl;
    }
    return 0;
}

4. 读者写者模型

在编写多线程的时候,有一种情况是十分常见的。那就是,有些公共数据修改的机会比较少。相比较改写,它们读的机会反而高的多。 通常而言,在读的过程中,往往伴随着查找的操作,中间耗时很长。给这种代码段加锁,会极
大地降低我们程序的效率。那么有没有一种方法,可以专门处理这种多读少写的情况呢? 有,那就是读写锁。

  • 条件
  1. 对数据,大部分的操作是读取,少量的操作是写入;
  2. 判断依据是:进行数据读取(消费)的一端是否会将数据取走,如果不取走,就可以考虑读写者模型。
  • 模型

这个模型有

三种关系:读者与读者、读者与写者、写者与写者

两种角色:读者与写者

一个交易场所:一段缓冲区

  • 读写者关系
  1. 读者VS写者:互斥、同步(写的时候无法读,读的时候无法写,但是写完可以马上读,读完可以马上写)

  2. 写者VS写者:互斥(写的时候其他线程无法写)

  3. 读者VS读者:无关系(只读不拿数据,所以可以同时进行)

这三种关系本质是使用锁来维护。

4.1 读写锁相关接口

创建一个读写锁:

在这里插入图片描述

释放一个读写锁:

在这里插入图片描述

以读者身份加锁:

在这里插入图片描述

以写者身份加锁:

在这里插入图片描述

  • 优先级

和消费生产模型一样,读写者模型也有优先级。但是不同点是:消费生产模型的优先级并不是固定的,而是通过队列的空与满来决定谁先谁后;而对于读写者模型,指定了谁先,那就是谁先,无论有没有读/写。

  1. 读者优先:读者写者同时来的时候,优先让读者进入访问。

  2. 写者优先:读者写者同时来的时候,比当前写者晚来的所有读者,都不要进入临界区访问了,等临界区中没有读者的时候,让写者先写入。理论上这样能避免写饥饿问题。但是目前有 BUG,导致表现行为和读者优先一致。

默认设置是读者优先,会导致写者饥饿情况。

但是读写者模型这样的处理会带来一个情况:读者多,写者少的问题。

5. 悲观锁与自旋锁

悲观锁也叫挂起等待特性锁,当线程访问临界资源需要的时间比较长时,其他线程会挂起等待。悲观锁是日常使用比较多的锁。

反之,自旋锁不会挂起等待,而是不断地循环,检测锁的状态,第一时间拿到锁资源。

选择悲观锁和自旋锁取决于线程访问临界资源的时间。

如果一个线程访问临界资源的时间很快,那么显然其他线程也能很快的得到锁并且访问临界资源,那其他线程就没必要挂起等待了,那么这个锁就适合于自旋锁。反之,如果一个线程访问临界资源的时间很长,采用循环检测的方法显然不合适,所以直接挂起等待是比较适合的。

那么线程如何得知自己会在临界区待多长时间呢?

只有程序员知道,因为线程进入临界区之后需要做什么,是由程序员实现的。

  • 自旋锁接口

申请和销毁:

在这里插入图片描述

加锁和解锁:

在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

久菜

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

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

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

打赏作者

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

抵扣说明:

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

余额充值