文章目录

  • 线程池
  • 线程池原理
  • 代码示例
  • 单例模式
  • 饿汉模式
  • 懒汉模式
  • 饿汉懒汉对比
  • 其他的锁


线程池

线程池原理

线程池是一种线程使用模式。在多线程应用中,若每有一个任务,线程就去调度相应的函数去创建,当任务过多时,每次都去调度且每次用完销毁,影响效率,加重CPU的负载;

而线程池是提前创建好的一批线程(不固定长度),没任务时就挂起等待,有任务分配时就被唤醒,等待分配任务,但也要具体分场景,例如任务时间短,且任务量大的时候

线程数量应该取决于可用的并发处理器、处理器内核、内存、网络sockets等的数量

linux监控线程池_linux监控线程池

应用场景:

  • 需要大量的线程来完成任务,且完成任务的时间比较短。 WEB服务器完成网页请求这样的任务,使用线程池技术是非常合适的。因为单个任务小,而任务数量巨大,你可以想象一个热门网站的点击次数。
    但对于长时间的任务,比如一个Telnet连接请求,线程池的优点就不明显了。因为Telnet会话时间比线程的创建时间大多了
  • 对性能要求苛刻的应用,比如要求服务器迅速响应客户请求
  • 接受突发性的大量请求,但不至于使服务器因此产生大量线程的应用
    突发性大量客户请求,在没 有线程池情况下,将产生大量线程,虽然理论上大部分操作系统线程数目最大值不是问题,短时间内产生大量线程 可能使内存到达极限,出现错误

代码示例

  1. 创建一个任务队列和线程池
  2. 外部向任务队列Push任务,任务队列唤醒线程池,并Pop给线程池,线程池分配出线程去执行任务
/*****************************************任务队列*************************************************/
#pragma once
#include <string>
#include <queue>
#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include <ctime>
#include <cstdlib>
#include "Task.hpp"

namespace dd
{
    const int Num = 5;
    template <class T>
    class ThreadPool
    {
public:

        ThreadPool(int num = Num) 
            : _num(num)
        {
            pthread_mutex_init(&_mtx, nullptr);
            pthread_cond_init(&_cond, nullptr);
        }
        ~ThreadPool() 
        {
            pthread_mutex_destroy(&_mtx);
            pthread_cond_destroy(&_cond);
        }
 		//加锁
        void Lock()
        {
            pthread_mutex_lock(&_mtx);
        }
		//解锁
        void Unlock()
        {
            pthread_mutex_unlock(&_mtx);
        }
		//判断空
        bool isEmpty()
        {
            return _task_queue.empty();
        }
        //唤醒条件变量的等待队列
        void WakeUp()
        {
            pthread_cond_signal(&_cond);
        }   
        //条件变量,挂起等待
        void wait()
        {
            pthread_cond_wait(&_cond,&_mtx);
        }

        
        //创建线程池
        void InitThreadPool()
        {
            pthread_t tid;
            for (int i = 0; i < num_; ++i)
            {
                pthread_create(&tid, nullptr, Rountine, (void*)this);
            }
        }

        // 在类内要让线程执行类内成员函数是不可行的
        // 普通成员函数中,参数默认一个this指针
        // 需要调用静态方法
        static void* Rountine(void* args)
        {
            //static函数不能访问类内成员和函数,所以让外面实例化的对象,调用Init通过this传进来
            ThreadPool<T>* tp = (ThreadPool<T>*)args;
            pthread_detach(pthread_self());
            while (true)
            {
                // 从任务队列中拿任务 为了能拿任务队列里的东西,需要传递this指针
                tp->Lock();
                while (tp->isEmpty()) 
                {
                    tp->wait();
                }
                //拿任务
                T t;
                tp->Poptask(&t);
                tp->Unlock();
                //拿完任务,任务就属于调用Pop的那个线程了,不是临界资源了
                //执行任务,执行任务时就不用占着锁了
                t.Run();
            }
        }        
        
        
		//放任务,放任务时访问的是临界资源:任务队列,所以要上锁
        void Pushtask(const T& in)
        {
            Lock();
            task_queue_.push(in);
            Unlock();
            WakeUp();
        }
		//拿任务,拿任务时的上下文是有锁的,所以不需要在这里上锁
        void Poptask(T* out)
        {
            *out = task_queue_.front();
            task_queue_.pop();
        }  
        
private:
        int _num;// 线程池中的线程数量
        std::queue<T> _task_queue;
        pthread_mutex_t _mtx;
        pthread_cond_t _cond;
    };
}

/**********************************************************************************************/
#pragma once
#include <iostream>
#include <pthread.h>
namespace dd
{

class Task
{
public:
    Task()
    {}
    Task(int x,int y,char op)
        :_x(x)
        ,_y(y)
        ,_op(op)
    {}

    int Run()
    {
        int ret = 0;
        switch(_op)
        {
            case '+':
                ret = _x + _y;
                break;
            case '-':
                ret = _x - _y;
                break;
            case '*':
                ret = _x * _y;
                break;
            case '/':
                ret = _x / _y;
                break;
            case '%':
                ret = _x % _y;
                break;
            default:
                break;
        }
        //std::cout << _x << _op << _y << " = " << ret << std::endl;
        std::cout << pthread_self() << ": " << _x << _op << _y << " = " << ret << std::endl;
    }
private:
    int _x;
    int _y;
    char _op;

};


}

/**********************************************************************************************/
#include "thread_pool.hpp"
#include "Task.hpp"
using namespace dd;
int main()
{
    ThreadPool<Task>* tp = new ThreadPool<Task>();
    //创建线程
    tp->InitThreadPool();
    srand((unsigned int)time(nullptr));
    while (true)
    {
        // 主线程push任务 真实情况中 任务一般是从网络来的
        Task t(rand() % 20 + 1, rand() % 10 + 1, "+-*/%"[rand() % 5]);
        tp->pushtask(t);
        // sleep(1);
    }

    return 0;
}

单例模式

单例模式是指在整个系统生命周期内,保证在内存中,一个类只会创建且仅创建一次对象的设计模式,确保该类的唯一性

  • 可以节省内存,节约资源,对于一般频繁创建和销毁对象的可以使用单例模式
  • 可以避免对资源的多重占用
    例如在很多服务器开发场景中, 经常需要让服务器加载很多的数据 (上百G) 到内存中. 此时往往要用一个单例的类来管理这些数据;也就是这么大的数据在内存只需要有一份,避免数据冗余(例如动态库)

单例类的特点

  • 构造函数和析构函数为私有类型,目的是禁止外部构造和析构
  • 拷贝构造函数和赋值构造函数是私有类型,目的是禁止外部拷贝和赋值,确保实例的唯一性
  • 类中的成员变量是静态的,静态的无论实例化多少对象,都只有(公用)一个静态成员变量
  • 类中要有一个获取实例的静态方法,可以全局访问

单例模式可以分为两种模式:饿汉模式、懒汉模式

饿汉模式

注意几点细节:

  • 需要将构造函数,拷贝构造函数,赋值运行符重载函数屏蔽,防止在外部实例化对象
  • 在类中创建一个静态类对象,该静态类对象,会在程序运行时创建,需要在类外初始化
  • 类中写一个接口(静态),返回静态类对象的地址。因为无法调用拷贝构造,不能直接返回类对象
template <typename T>
class Singleton 
{
	static T data;
    //static Singleton<T> data
    SingelTon() = delete;
	SingelTon(const SingelTon& st) = delete;;
	SingelTon& operator=(const SingelTon& st) = delete;
    
public:
	static T* GetInstance() 
    {
		return &data;
	}
};
template<class T>
T Singleton::data = /**/;
//Singleton<T> Singleton::data = /**/;

吃完饭, 立刻洗碗, 这种就是饿汉方式. 因为下一顿吃的时候可以立刻拿着碗就能吃饭;也就是系统一运行,就初始化创建实例,当需要时,直接调用即可。这种方式本身就可以保证线程安全,没有多线程的线程安全问题

懒汉模式

懒汉模式注意的细节:

  • 需要将构造函数,拷贝构造函数,赋值运行符重载函数屏蔽,防止在外部实例化对象。
  • 在类中成员包含一个类对象指针,而不是对象。
  • 类中写一个接口(静态),用户返回创建后的对象(但只能调用一次)
  • 因为是需要时才会创建,所以需要考虑线程安全问题,保证多线程下只能创建一个对象
template <class T>
class Singleton 
{
	volatile static T* inst; // 需要设置 volatile 关键字, 否则可能被编译器优化.
	static std::mutex lock;
    Singleton()
    {
        pthread_mutex_init(&lock,nullptr);
    }
    ~Singleton()
    {
        pthread_mutex_destroy(&lock);
    }
    Singleton(const Singleton<T>& sl) = delete;
    Singleton<T>& operator=(Singleton<T> sl) = delete;
    
public:
	static T* GetInstance() 
    {
		if (inst == NULL) 
        { 				// 双重判定空指针, 降低争锁冲突, 提高性能.
			lock.lock(); // 使用互斥锁, 保证多线程情况下也只调用一次 new.
			if (inst == NULL) 
            {
				inst = new T();
			}
			lock.unlock();
		}
		return inst;
	}
};
//类外初始化静态成员
template <class T>
T* Singleton::inst = nullptr;

吃完饭, 先把碗放下, 然后下一顿饭用到这个碗了再洗碗, 就是懒汉方式;也就是系统运行中,实例并不存在,只有当需要使用该实例时,才会去创建并使用实例这种方式要考虑线程安全问题

饿汉懒汉对比

饿汉模式

  • 优点:
  • 实现简单,不需要考虑线程安全问题。
  • 缺点:
  • 启动慢。如果实例的对象占用资源很多,在启动时需要加载。
  • 如果多个单例类对象,在程序启动时实例对象的顺序不确定。如果对象之间有依赖关系,就麻烦了。

懒汉模式

  • 优点:
  • 启动快。在需要时才会实例化对象,加载资源。
  • 多个单例类对象,实例化的顺序可以确定。取决于调用类的函数的顺序。
  • 缺点:
  • 实现复杂。需要考虑线程安全问题。

其他的锁

  • 悲观锁:在每次取数据时,总是担心数据会被其他线程修改,所以会在取数据前先加锁(读锁,写锁,行 锁等),当其他线程想要访问数据时,被阻塞挂起。
  • 乐观锁:每次取数据时候,总是乐观的认为数据不会被其他线程修改,因此不上锁。但是在更新数据前, 会判断其他数据在更新前有没有对数据进行修改。主要采用两种方式:版本号机制和CAS操作。
  • CAS操作:当需要更新数据时,判断当前内存值和之前取得的值是否相等。如果相等则用新值更新。若不等 则失败,失败则重试,一般是一个自旋的过程,即不断重试。