详细介绍:【Linux】线程池

Alt

个人主页Quitecoder

专栏linux笔记仓

Alt

01.线程池

线程池是一种线程使用模式,线程过多会带来调度开销,进而影响缓存局部性和整体性能。而线程池维护着多个线程,等待着监督管理者分配可并发执行的任务。这避免了在处理短时间任务时创建与销毁线程的代价。线程池不仅能够保证内核的充分利用,还能防止过分调度。可用线程数量应该取决于可用的并发处理器、处理器内核、内存、网络sockets等的数量

线程池示例:

  1. 创建固定数量线程池,循环从任务队列中获取任务对象
  2. 获取到任务对象后,执行任务对象中的任务接口

代码基本结构

#pragma once
#include <iostream>
  #include <unistd.h>
    #include <string>
      #include <vector>
        #include "mythread.hpp"
        #include <queue>
          #include<functional>
            using namespace Threadmoudle;
            static const int gdefaultnum = 5;
            template <
            typename T>
            class ThreadPool
            {
            void Lockqueue()
            {
            pthread_mutex_lock(&_qmutex);
            }
            void UnLockqueue()
            {
            pthread_mutex_unlock(&_qmutex);
            }
            void Wakeup()
            {
            pthread_cond_signal(&_cond);
            }
            bool Isempty()
            {
            return _taskqueue.empty();
            }
            void HandlerTask()//this
            {
            while (true)
            {
            Lockqueue();
            while (Isempty())
            {
            pthread_cond_wait(&_cond, &_qmutex);
            }
            T t = _taskqueue.front();
            _taskqueue.pop();
            UnLockqueue();
            t();
            // 处理任务不能在临界区
            }
            }
            public:
            ThreadPool(int threadnum = gdefaultnum) : _threadnum(threadnum), _isrunning(false), _sleep_thread_num(0)
            {
            pthread_mutex_init(&_qmutex, nullptr);
            pthread_cond_init(&_cond, nullptr);
            }
            void Init()
            {
            func_t func= std::bind(&ThreadPool::HandlerTask,this);
            for (int i = 0; i < _threadnum;
            ++i)
            {
            std::string threadname = "thread-" + std::to_string(i + 1);
            _threads.emplace_back(threadname, func);
            }
            }
            void Start()
            {
            for (auto &e : _threads)
            {
            e.Start();
            }
            }
            void Stop()
            {
            }
            void AddTask(const T &task)
            {
            Lockqueue();
            _taskqueue.push(task);
            if (_sleep_thread_num >
            0)
            Wakeup();
            UnLockqueue();
            }
            ~ThreadPool()
            {
            pthread_mutex_destroy(&_qmutex);
            pthread_cond_destroy(&_cond);
            }
            private:
            int _threadnum;
            std::vector<Thread> _threads;
              std::queue<T> _taskqueue;
                bool _isrunning;
                int _sleep_thread_num;
                pthread_mutex_t _qmutex;
                pthread_cond_t _cond;
                };

这个线程池采用生产者-消费者模型

  • 生产者:通过AddTask方法向任务队列添加任务
  • 消费者:工作线程从队列中取出并执行任务
void Init()
{
func_t func = std::bind(&ThreadPool::HandlerTask, this);
for (int i = 0; i < _threadnum;
++i)
{
std::string threadname = "thread-" + std::to_string(i + 1);
_threads.emplace_back(threadname, func);
}
}

这里使用std::bind将成员函数HandlerTask绑定到线程,解决了之前版本中线程执行错误函数的问题。

任务处理循环 (HandlerTask)

void HandlerTask()
{
while (true)
{
Lockqueue();
while (Isempty())
{
pthread_cond_wait(&_cond, &_qmutex);
}
T t = _taskqueue.front();
_taskqueue.pop();
UnLockqueue();
t();
// 处理任务不能在临界区
}
}

这是工作线程的核心逻辑:

  1. 获取锁:保护对任务队列的访问
  2. 等待条件:如果队列为空,线程进入等待状态
  3. 获取任务:从队列头部取出任务
  4. 释放锁:尽快释放锁,让其他线程可以访问队列
  5. 执行任务:在锁外执行任务,避免阻塞其他线程

任务添加 (AddTask)

void AddTask(const T &task)
{
Lockqueue();
_taskqueue.push(task);
if (_sleep_thread_num >
0)
Wakeup();
UnLockqueue();
}

向队列添加新任务,并唤醒等待的线程(如果有)。

互斥锁 (_qmutex)

  • 保护任务队列的并发访问
  • 确保同一时间只有一个线程可以操作队列

条件变量 (_cond)

  • 用于线程间通信
  • 当队列为空时,工作线程等待条件变量
  • 当新任务加入时,通知等待的线程
int main()
{
//std::unique_ptr<ThreadPool> tp = std::make_unique<ThreadPool>();
  ThreadPool<Task>
    * tp = new ThreadPool<Task>
      ();
      tp->
      Init();
      tp->
      Start();
      while(true)
      {
      sleep(1);
      Task t(1,1);
      tp->
      AddTask(t);
      sleep(1);
      }
      return 0;
      }

现在启动线程,每一秒投递一个任务,但是现在发现线程并没有处理任务,代码中还有需要修改的地方,代码中使用了_sleep_thread_num变量

void HandlerTask()//this
{
while (true)
{
Lockqueue();
while (Isempty())
{
_sleep_thread_num++;
pthread_cond_wait(&_cond, &_qmutex);
_sleep_thread_num--;
}
T t = _taskqueue.front();
_taskqueue.pop();
UnLockqueue();
t();
// 处理任务不能在临界区
std::cout<<
"done"<<std::endl;
}
}

在这里插入图片描述
我们再打印出是哪个线程执行的任务只需要修改回调函数的参数,

using func_t =std::function<
void(const std::string&
)>
;
void HandlerTask(const std::string & name)//this
func_t func= std::bind(&ThreadPool::HandlerTask,this,std::placeholders::_1);

最后我们完善线程池的退出函数

void HandlerTask(const std::string &name) // this
{
while (true)
{
Lockqueue();
while (Isempty() && _isrunning)
{
_sleep_thread_num++;
pthread_cond_wait(&_cond, &_qmutex);
_sleep_thread_num--;
}
if (Isempty() &&
!_isrunning)
{
std::cout << name <<
" quit" << std::endl;
UnLockqueue();
break;
}
T t = _taskqueue.front();
_taskqueue.pop();
UnLockqueue();
t();
// 处理任务不能在临界区
std::cout << name <<
": " << t.result() << std::endl;
}
}

这里用上isrunning状态量,如果有任务,线程就不让他退,让它一直运行

这里只判定一种情况让它退出,任务队列为空并且线程池退出的时候,让线程退出

void WakeupAll()
{
pthread_cond_broadcast(&_cond);
}
void Stop()
{
Lockqueue();
_isrunning=false;
WakeupAll();
UnLockqueue();
}

这里需要唤醒所有的线程

日志文件

日志:软件运行的记录信息,特定的格式

[日志等级][pid][filename][filenumber][time] 日志内容(可变参数)

日志等级:DEBUG,INFO,WARNING,ERROR,FATAL

等级数值说明使用场景
DEBUG0调试信息详细的调试信息,生产环境通常关闭
INFO1普通信息程序运行状态信息
WARNING2警告信息不影响程序运行但需要注意的情况
ERROR3错误信息程序错误但可以继续运行
FATAL4严重错误导致程序无法继续运行的错误
enum
{
DEBUG=1,
INFO,
WARNING,
ERROR,
FATAL
};
class logmessage
{
public:
std::string _level;
pid_t _id;
std::string _filename;
int _filenumber;
std::string _curr_time;
std::string _message_info;
};

这是日志信息,我们现在创建日志类,完善信息函数:

class Log
{
public:
Log()
{
}
void logMessage(std::string filename,int filenumber,int level,const char *format,...)
{
logmessage lg;
lg._level =LevelToString(level);
lg._id =getpid();
lg._filename=filename;
lg._filenumber=filenumber;
lg._curr_time = GetCurrTime();
va_list ap;
va_start(ap,format);
char message_info[1024];
vsnprintf(message_info,sizeof(message_info),format,ap);
va_end(ap);
lg._message_info=message_info;
}
~Log()
{
}
private:
};
std::string LevelToString(int level)
{
switch(level)
{
case DEBUG:
return "DEBUG";
case INFO:
return "INFO";
case WARNING:
return "WARNING";
case ERROR:
return "ERROR";
case FATAL:
return "FATAL";
default:
return "UNKNOWN";
}
}

这里我们将传进来的枚举类型转换为字符串

获取时间函数,我们这里调用系统调用

std::string GetCurrTime()
{
time_t now = time(nullptr);
struct tm* curr_time = localtime(&now);
char buffer[128];
snprintf(buffer, sizeof(buffer), "%d-%02d-%02d %02d:%02d:%02d",
curr_time->tm_year + 1900,
curr_time->tm_mon + 1,
curr_time->tm_mday,
curr_time->tm_hour,
curr_time->tm_min,
curr_time->tm_sec);
return buffer;
}

localtime传入时间戳,返回类型为struct tm*的结构体,这个结构体包含了当前的年月日时分秒信息,我们打入缓冲区中(这个tm_year是减去1900的,我们这里加上1900,月份是从0到11的,我们加一)
测试:
在这里插入图片描述
最后这里捕获可变参数
在这里插入图片描述
这里打印日志信息,我们可以定义类型,分别为向文件打印和向显示器打印

#define SCREEN_TYPE 1
#define FILE_TYPE 2
void FlushLogToScreen(const logmessage& lg)
{
printf("[%s][%d][%s][%d][%s] %s",
lg._level.c_str(),
lg._id,
lg._filename.c_str(),
lg._filenumber,
lg._curr_time.c_str(),
lg._message_info
);
}
void FlushLogToFile(const logmessage& lg)
{
std::ofstream out(_logfile);
if(!out.is_open()) return;
char logtxt[2048];
snprintf(logtxt,sizeof(logtxt),"[%s][%d][%s][%d][%s] %s",
lg._level.c_str(),
lg._id,
lg._filename.c_str(),
lg._filenumber,
lg._curr_time.c_str(),
lg._message_info
);
out.write(logtxt,strlen(logtxt));
out.close();
}

现在给这个日志文件加上线程安全机制,保证在多线程打印正确

class LockGuard
{
public:
LockGuard(pthread_mutex_t *mutex):_mutex(mutex)
{
pthread_mutex_lock(_mutex);
}
~LockGuard()
{
pthread_mutex_unlock(_mutex);
}
private:
pthread_mutex_t *_mutex;
};
pthread_mutex_t glock = PTHREAD_ADAPTIVE_MUTEX_INITIALIZER_NP;
void FlushLog(const logmessage& lg)
{
LockGuard LockGuard(&glock);
switch(_type)
{
case SCREEN_TYPE:
FlushLogToScreen(lg);
break;
case FILE_TYPE:
FlushLogToFile(lg);
break;
}
}

现在我们就可以按照这样的调用进行打印日志信息:

Log lg;
lg.Enable(FILE_TYPE);
lg.logMessage("main.cc",10,DEBUG,"hello %d\n",1000);

但是每次这样传文件名和行号未免有点太麻烦了,我们这里可以用预处理符

lg.logMessage(__FILE__,__LINE__,DEBUG,"hello %d\n",1000);

这两个预处理符在编译的时候会被自动编译为当前文件名和行号

现在继续完善,让调用更直白简单

在这里插入图片描述

Log lg;
#define LOG(Level,Format,...) \
do \
{ \
lg.logMessage(__FILE__,__LINE__,Level,Format,__VA_ARGS__);\

}while(0)

在Log头文件直接定义全局的对象和宏函数

将来直接调用宏调用

LOG(DEBUG,"hello %d\n",1000);
#define EnableScreen() \
do \
{ \
lg.Enable(SCREEN_TYPE); \

} while (0)
#define EnableFILE() \
do \
{ \
lg.Enable(FILE_TYPE); \

} while (0)
};

我们将来的日志只需要调用这三个宏接口即可

02.线程安全的单例模式

单例模式的特点
某些类, 只应该具有一个对象(实例), 就称之为单例.

在很多服务器开发场景中, 经常需要让服务器加载很多的数据 (上百G) 到内存中. 此时往往要用一个单例的类来管理这些数据

饿汉实现方式和懒汉实现方式

吃完饭, 立刻洗碗, 这种就是饿汉方式. 因为下一顿吃的时候可以立刻拿着碗就能吃饭.
吃完饭, 先把碗放下, 然后下一顿饭用到这个碗了再洗碗, 就是懒汉方式.

懒汉方式最核心的思想是 “延时加载”. 从而能够优化服务器的启动速度

饿汉方式实现单例模式

template <
typename T>
class Singleton
{
static T data;
public:
static T* GetInstance() {
return &data;
}
};

懒汉方式实现单例模式

template <
typename T>
class Singleton
{
static T* inst;
public:
static T* GetInstance() {
if (inst == NULL) {
inst = new T();
}
return inst;
}
};

存在一个严重的问题, 线程不安全.
第一次调用 GetInstance 的时候, 如果两个线程同时调用, 可能会创建出两份 T 对象的实例.
但是后续再次调用, 就没有问题了.

懒汉方式实现单例模式(线程安全版本)

template <
typename T>
class Singleton
{
volatile static T* inst;
// 需要设置 volatile 关键字, 否则可能被编译器优化.
static std::mutex lock;
public:
static T* GetInstance() {
if (inst == NULL) {
// 双重判定空指针, 降低锁冲突的概率, 提高性能.
lock.lock();
// 使用互斥锁, 保证多线程情况下也只调用一次 new.
if (inst == NULL) {
inst = new T();
}
lock.unlock();
}
return inst;
}
};

注意事项:

  1. 加锁解锁的位置
  2. 双重 if 判定, 避免不必要的锁竞争
  3. volatile关键字防止过度优化
posted @ 2025-09-09 19:49  yjbjingcha  阅读(12)  评论(0)    收藏  举报