hello~ 很高兴见到大家! 这次带来的是Linux系统中关于线程这部分的一些知识点,如果对你有所帮助的话,可否留下你宝贵的三连呢?
个 人 主 页 : 默|笙

文章目录
- 一、日志
-
- [1.1 认识接口](#1.1 认识接口)
- [1.2 代码实现](#1.2 代码实现)
-
- [1. 策略模式](#1. 策略模式)
- [2. 可变参数流式输出与创建内部类的作用](#2. 可变参数流式输出与创建内部类的作用)
- 二、线程池
-
- [2.1 介绍](#2.1 介绍)
- [2.2 代码实现](#2.2 代码实现)
- [2.3 单例模式](#2.3 单例模式)
一、日志
在实际工程项目中,日志系统几乎是必不可少的基础组件。所谓日志,就是程序在运行过程中,按一定格式持续输出的运行信息、状态记录、关键流程与异常提示,它能够完整保留程序的执行轨迹。通过日志,我们可以清晰复现问题现场,快速定位 bug、追踪异常、分析逻辑流程,从而高效完成问题排查与程序调试。
1.1 认识接口
cpp
[可读性很好的时间] [⽇志等级] [进程pid] [打印对应⽇志的⽂件名][⾏号] - 消息内容,⽀持
可变参数
[2024-08-04 12:27:03] [DEBUG] [202938] [main.cc] [16] - hello world
[2024-08-04 12:27:03] [DEBUG] [202938] [main.cc] [17] - hello world
[2024-08-04 12:27:03] [DEBUG] [202938] [main.cc] [18] - hello world
[2024-08-04 12:27:03] [DEBUG] [202938] [main.cc] [20] - hello world
[2024-08-04 12:27:03] [DEBUG] [202938] [main.cc] [21] - hello world
[2024-08-04 12:27:03] [WARNING] [202938] [main.cc] [23] - hello world
- 我们希望日志输出信息是上面的格式,第一步就是需要得到当前的时间,我们可以通过系统提供的接口来得到:


- gettimeofday 接口可以用来获取当前的高精度时间。第一个参数是输出型参数,类型为 struct timeval,该结构体包含两个成员变量:tv_sec 表示从 1970年1月1日 00:00:00 UTC 到现在的秒数 ;tv_usec 表示不足一秒的剩余微秒数。gettimeofday 的第二个参数是时区信息,类型为 struct timezone*,现在已经废弃、几乎不用,传 NULL /nullptr 即可。
- 使用要包含头文件 sys/time.h。


cpp
struct tm
{
int tm_sec; // 秒:0-59
int tm_min; // 分:0-59
int tm_hour; // 时:0-23
int tm_mday; // 日:1-31
int tm_mon; // 月:0-11(注意:从0开始!)
int tm_year; // 年:从1900开始算
int tm_wday; // 星期几:0-6(周日是0)
int tm_yday; // 一年中的第几天:0-365
int tm_isdst; // 夏令时标识
};
-
获取到秒级时间戳之后,需要将其转换为我们日常使用的年、月、日、时、分、秒等格式。这个转换工作可以通过 localtime_r 接口完成。localtime_r 与 localtime 功能完全一致,区别在于:localtime 是不可重入函数,localtime_r 是线程安全的可重入函数。在 Linux 下,多线程程序必须使用 localtime_r。第一个参数:输入,需要转换的时间戳(time_t 类型);第二个参数:输出,传入一个 struct tm 结构体变量的地址,转换完成后的数据会存储在该结构体中。
-
使用必须包含头文件 time.h。
1.2 代码实现
cpp
#ifndef _LOGGER_HPP
#define _LOGGER_HPP
#pragma once
#include <time.h>
#include <sys/time.h>
#include <iostream>
#include <fstream>
#include <filesystem>
#include <sstream>
#include <unistd.h>
#include <cstdlib>
#include "Mutex.hpp"
namespace NS_LOG_MODULE
{
enum class LogLevel
{
INFO,
WARNING,
ERROR,
FATAL,
DEBUG
};
std::string LogLevel2Message(LogLevel level)
{
switch(level)
{
case LogLevel::INFO:
return "INFO";
case LogLevel::WARNING:
return "WARNING";
case LogLevel::ERROR:
return "ERROR";
case LogLevel::FATAL:
return "FATAL";
default:
return "DEBUG";
}
}
std::string GetCurrentTime()
{
struct timeval ctime;
int n = gettimeofday(&ctime, nullptr);
(void)n;
struct tm _tm;
localtime_r(&(ctime.tv_sec), &_tm);
char timestr[128];
snprintf(timestr, sizeof timestr, "%04d-%02d-%02d %02d:%02d:%02d.%ld",
_tm.tm_year + 1900,
_tm.tm_mon + 1,
_tm.tm_mday,
_tm.tm_hour,
_tm.tm_min,
_tm.tm_sec,
ctime.tv_usec);
return timestr;
}
//策略模式
class LogStrategy
{
public:
virtual ~LogStrategy() = default;
virtual void SyncLog(const std::string &message) = 0;
};
// 向控制器输出
class ConsoleStrategy : public LogStrategy
{
public:
void SyncLog(const std::string &message) override
{
LockGuard lockguard(_lock);
std::cout << message << std::endl;
}
~ConsoleStrategy()
{
}
private:
Mutex _lock;
};
const std::string defaultpath = "./log";
const std::string defaultfilename = "log.txt";
// 向指定文件输出
class FileLogStrategy : public LogStrategy
{
public:
FileLogStrategy(const std::string &logfilename = defaultfilename, const std::string &logpath = defaultpath)
: _logfilename(logfilename),
_logpath(logpath)
{
{
LockGuard lockguard(_lock);
// 路径如果存在就返回
if (std::filesystem::exists(_logpath))
return;
// 不存在就创建
try
{
std::filesystem::create_directories(_logpath);
}
catch (const std::filesystem::filesystem_error &e)
{
std::cerr << e.what() << '\n';
}
}
}
void SyncLog(const std::string &message) override
{
{
LockGuard lockguard(_lock);
if (!_logpath.empty() && _logpath.back() != '/')
_logpath += "/";
std::string targetlog = _logpath + _logfilename; // 合成目标路径
std::ofstream out(targetlog, std::ios::app); // 以追加方式写入targetlog路径所在文件
if (!out.is_open())
{
std::cerr << "open" << targetlog << "failed" << std::endl;
}
out << message << "\n"; // 建议使用\n而不是endl,后者是换行+刷新,磁盘刷新会特别慢
out.close();
}
}
~FileLogStrategy()
{
}
private:
std::string _logfilename;
std::string _logpath;
Mutex _lock;
};
//根据不同的策略,刷新到不同位置
class Logger
{
public:
Logger()
{
UseConsoleStrategy();
}
void UseConsoleStrategy()
{
_strategy = std::make_unique<ConsoleStrategy>();
}
void UseFileLogStrategy()
{
_strategy = std::make_unique<FileLogStrategy>();
}
class LogMessage
{
public:
LogMessage(LogLevel level, std::string& filename, int line, Logger& logger)
: _level(level),
_current_time(GetCurrentTime()),
_pid(getpid()),
_filename(filename),
_line(line),
_logger(logger)
{
//先构建出左半部分
std::stringstream ss;
ss << "[" << _current_time << "] "
<< "[" << LogLevel2Message(_level) << "] "
<< "[" << _pid << "] "
<< "[" << _filename << "] "
<< "[" << _line << "] ";
_loginfo = ss.str();
}
template<class T>
LogMessage& operator<<(const T& info)
{
std::stringstream ss;
ss << info;
_loginfo += ss.str();
return *this;
}
~LogMessage()
{
if (_logger._strategy)
{
_logger._strategy->SyncLog(_loginfo);
}
}
private:
LogLevel _level;
std::string _current_time;
pid_t _pid;
std::string _filename;
int _line;
std::string _loginfo;//一条完整的日志信息
//引用外部的logger对象
Logger& _logger;
};
//采用拷贝
LogMessage operator()(LogLevel level, std::string filename, int line)
{
return LogMessage(level, filename, line, *this);
}
private:
std::unique_ptr<LogStrategy> _strategy; // 刷新策略
};
//日志对象,全局使用
Logger logger;
#define ENABLE_CONSOLE_LOG_STRATEGY() logger.UseConsoleStrategy();
#define ENABLE_FILE_LOG_STRATEGY() logger.UseFileLogStrategy();
#define LOG(level) logger(level, __FILE__, __LINE__)
}
#endif
1. 策略模式
- 策略模式就是将不同的算法或行为封装成独立的类,使它们可以在程序运行时动态替换 。比如我们日志模块中,向控制台打印和向文件打印就是两种不同的输出策略。通过继承 + 多态,让基类指针可以指向不同的子类对象,并根据需求动态切换具体策略,而不需要修改上层调用代码,从而达到灵活扩展、解耦的目的。
- 策略模式就像一个人去学校:他可以选择不同的交通工具,不同的交通工具就是不同的策略。人(要输出的日志内容)是不变的,变的只是去学校的方式(日志输出到哪里:控制台 / 文件)。通过继承 + 多态,我们把不同的输出方式封装成独立策略类,在程序运行时可以随时切换策略,而不用修改日志本身的逻辑。
- 基类的作用只有两个:统一接口(规定所有策略必须做什么),实现多态(让基类指针可以指向任意子类)。
2. 可变参数流式输出与创建内部类的作用

- 日志左半部分格式固定,包含时间戳、日志等级、进程 ID、文件名、行号等信息。因此在创建 LogMessage 临时对象时,就可以直接预先构建好固定的日志头部,右半部分的自定义内容再通过流式 << 动态拼接,最终形成一条完整日志。
- operator<< 必须返回自身引用 LogMessage&,而不能返回值对象。如果返回值,会发生拷贝,导致后续内容拼接在临时拷贝对象上,而非原临时对象,最终造成日志内容断裂、丢失。只有返回引用,才能保证所有内容都拼接在同一个临时对象上。
- LogMessage 作为内部类,核心作用是:利用析构函数自动输出日志,实现用完即输出的机制;封装 operator<< 实现流式调用;同时与 Logger 职责分离,不污染 Logger 类结构,让设计更清晰、更模块化。
- 全局 logger 的作用:让日志在程序任何地方都能直接使用,统一管理输出策略,不用传递、不用创建,简单、方便、统一。
二、线程池
2.1 介绍

- 线程池 = 预先创建好的一批常驻工作线程 + 线程安全的任务队列 + 统一管理工具。使用线程池时,用户将任务提交到任务队列中;工作线程会互斥访问任务队列这一临界资源(通过互斥锁保护,避免并发冲突),有序获取并执行任务;当队列为空时,线程会通过条件变量进入休眠状态(而非空转消耗 CPU),等待新任务到来时被唤醒,全程保持待命状态,实现线程复用。
2.2 代码实现
- 代码要好好看一遍,要注意的点都写在注释里面:
cpp
#pragma once
#include "Logger.hpp"
#include "Mutex.hpp"
#include "Thread.hpp"
#include "Cond.hpp"
#include <queue>
#include <vector>
#include <iostream>
namespace NS_THREAD_POOL_MOUDLE
{
using namespace NS_THREAD_MODULE;
using namespace NS_LOG_MODULE;
const int defaultnum = 5;
// void Task()
// {
// char name[128];
// pthread_getname_np(pthread_self(), name, sizeof name);
// while (true)
// {
// LOG(LogLevel::DEBUG) << "我是一个线程,我正在运行。" << name;
// }
// }
template <class T>
class ThreadPool
{
public:
void HandlerTask()
{
char name[128];
pthread_getname_np(pthread_self(), name, sizeof name);
//除非用户主动关闭线程池,那么线程池里面所有的线程都是要待命的,等待用户往任务队列里面丢任务
while (true)
{
T task;
{
LockGuard lockguard(_mutex);
//_mutex.Lock();
//任务池是空的且线程池还在运行才能够让线程去休眠
while (_tasks.empty() && _isrunning)
{
_slaver_sleep_num++;
_cond.Wait(_mutex);
_slaver_sleep_num--;
}
//当线程池终止且没有任务要处理的时候再退出循环
if (!_isrunning && _tasks.empty())
{
//_mutex.Unlock();
break;
}
//有任务就获得任务
task = _tasks.front();
_tasks.pop();
//_mutex.Unlock();
}
LOG(LogLevel::INFO) << name << "线程执行任务";
// 执行任务,不需要在锁里,不然效率太低
task(); // 统一执行方式,类要求就重载(),函数,lambda表达式就正常用
LOG(LogLevel::DEBUG) << task.Result();
}
// 线程执行完任务后退出
LOG(LogLevel::INFO) << name << "线程即将退出";
}
ThreadPool(int slaver_num = defaultnum)
: _slaver_num(slaver_num),
_isrunning(false),
_slaver_sleep_num(0)
{
for (int idx = 0; idx < _slaver_num; idx++)
{
//通过lambda表达式,让传递的函数符合要求,封装的Thread类里面要求是void(无参无返回值)函数
_slavers.emplace_back([this]()
{ this->HandlerTask(); });
}
}
void Start()
{
if (_isrunning)
{
LOG(LogLevel::WARNING) << "Thread Pool Is Already Running";
return;
}
_isrunning = true; // 不能放到后面,否则可能导致重复启动问题
// 启动所有的线程
for (auto &slaver : _slavers)
{
slaver.Start();
}
}
void Stop()
{
// version1,太过于粗暴
// if (!_isrunning)
// {
// LOG(LogLevel::WARNING) << "Thread Pool Is Not Running";
// return;
// }
// for (auto &slaver : _slavers)
// {
// slaver.Die();
// }
// _isrunning = false; // 先做事后标记
//version2
_mutex.Lock();
_isrunning = false;
//把线程全部唤醒,让它们去处理任务
if (_slaver_num > 0)
_cond.Broadcast();
_mutex.Unlock();
}
void Wait()
{
for (auto &slaver : _slavers)
{
slaver.Join();
}
}
void Enqueue(const T &in)
{
_mutex.Lock();
_tasks.push(in);
if (_slaver_sleep_num > 0)
_cond.Signal();
_mutex.Unlock();
}
~ThreadPool() {}
private:
int _slaver_num; // 所要创建的线程的数量
bool _isrunning; // 线程池是否启动
std::queue<T> _tasks; // 任务队列,临界资源
std::vector<Thread> _slavers; // 线程池
Cond _cond; // 条件变量
Mutex _mutex; // 保护任务队列的锁
int _slaver_sleep_num; // 进入等待队列的线程的数量
};
}
2.3 单例模式
- 单例模式:一个类在程序的整个生命周期中,只能创建唯一的一个实例对象,不允许创建多个对象,这种设计模式就叫做单例模式。我们自己实现的线程池是最典型的单例应用场景:全局只需要一个线程池统一管理线程、任务队列和调度,绝不应该创建多个线程池实例。
- 单例模式有两种经典实现:饿汉模式和懒汉模式。程序中的全局变量、静态变量存储在静态存储区(数据段),会在程序启动、main 函数执行之前就完成初始化和内存分配;而栈、堆空间则是在程序运行时动态分配。
- 饿汉实现:在程序启动之初就创建好单例对象(无论是否需要,直接提前创建),是一种空间换时间的设计。
- 懒汉实现:延迟加载,只有当用户第一次需要使用单例对象时,才真正创建实例,是一种时间换空间的设计。
- 懒汉实现单例模式代码,代码要好好看一遍,要注意的点都写在注释里面:
cpp
#pragma once
#include "Logger.hpp"
#include "Mutex.hpp"
#include "Thread.hpp"
#include "Cond.hpp"
#include <queue>
#include <vector>
#include <iostream>
namespace NS_THREAD_POOL_MOUDLE
{
using namespace NS_THREAD_MODULE;
using namespace NS_LOG_MODULE;
const int defaultnum = 5;
// void Task()
// {
// char name[128];
// pthread_getname_np(pthread_self(), name, sizeof name);
// while (true)
// {
// LOG(LogLevel::DEBUG) << "我是一个线程,我正在运行。" << name;
// }
// }
template <class T>
class ThreadPool
{
//私有化
private:
void HandlerTask()
{
char name[128];
pthread_getname_np(pthread_self(), name, sizeof name);
while (true)
{
T task;
{
LockGuard lockguard(_mutex);
//_mutex.Lock();
// 任务池是空的且线程池还在运行才能够让线程去休眠
while (_tasks.empty() && _isrunning)
{
_slaver_sleep_num++;
_cond.Wait(_mutex);
_slaver_sleep_num--;
}
// 当线程池终止且没有任务要处理的时候再退出循环
if (!_isrunning && _tasks.empty())
{
//_mutex.Unlock();
break;
}
// 有任务就获得任务
task = _tasks.front();
_tasks.pop();
//_mutex.Unlock();
}
LOG(LogLevel::INFO) << name << "线程执行任务";
// 执行任务,不需要在锁里,不然效率太低
task(); // 统一执行方式,类要求就重载(),函数,lambda表达式就正常用
LOG(LogLevel::DEBUG) << task.Result();
}
// 线程执行完任务后退出
LOG(LogLevel::INFO) << name << "线程即将退出";
}
ThreadPool(int slaver_num = defaultnum)
: _slaver_num(slaver_num),
_isrunning(false),
_slaver_sleep_num(0)
{
for (int idx = 0; idx < _slaver_num; idx++)
{
_slavers.emplace_back([this]()
{ this->HandlerTask(); });
}
}
//禁止拷贝和赋值构造,私有默认构造,这样用户只能通过我们提供的接口创建和调用线程池
ThreadPool(const ThreadPool<T> &) = delete;
ThreadPool &operator=(const ThreadPool<T> &) = delete;
public:
//提供创建和得到线程池的接口,设置成static
//此函数属于一整个类,而不是某个实例,也就是说不需要创建实例就可以进行调用,不然用户无法创建单例。
static ThreadPool* Instance()
{
//双重判断,防止在此线程申请的过程中,有另一个线程也过来进行申请。
//外层 if → 为了提高效率(不加锁快速返回),防止已经有单例了结果多个线程还是在争抢锁去执行接下来的代码
if (_instance == nullptr)
{
_lock.Lock();
//内层 if → 为了保证线程安全(防止创建多个单例)
if (_instance == nullptr)
{
//第一次调用,需要创建单例
_instance = new ThreadPool<T>();
_instance->Start();
LOG(LogLevel::INFO) << "第一次创建线程池对象";
}
_lock.Unlock();
}
return _instance;
}
void Start()
{
if (_isrunning)
{
LOG(LogLevel::WARNING) << "Thread Pool Is Already Running";
return;
}
_isrunning = true; // 不能放到后面,否则可能导致重复启动问题
// 启动所有的线程
for (auto &slaver : _slavers)
{
slaver.Start();
}
}
void Stop()
{
// version1,太过于粗暴
// if (!_isrunning)
// {
// LOG(LogLevel::WARNING) << "Thread Pool Is Not Running";
// return;
// }
// for (auto &slaver : _slavers)
// {
// slaver.Die();
// }
// _isrunning = false; // 先做事后标记
// version2
_mutex.Lock();
_isrunning = false;
// 把线程全部唤醒,让它们去处理任务
if (_slaver_num > 0)
_cond.Broadcast();
_mutex.Unlock();
}
void Wait()
{
for (auto &slaver : _slavers)
{
slaver.Join();
}
}
void Enqueue(const T &in)
{
_mutex.Lock();
_tasks.push(in);
if (_slaver_sleep_num > 0)
_cond.Signal();
_mutex.Unlock();
}
~ThreadPool() {}
private:
int _slaver_num; // 所要创建的线程的数量
bool _isrunning; // 线程池是否启动
std::queue<T> _tasks; // 任务队列,临界资源
std::vector<Thread> _slavers; // 线程池
Cond _cond; // 条件变量
Mutex _mutex; // 保护任务队列的锁
int _slaver_sleep_num; // 进入等待队列的线程的数量
static ThreadPool<T>* _instance; //单例本身
static Mutex _lock; //这把锁用来保护单例,单例也是一种临界资源
};
//静态变量需要在类外进行初始化
//模板类的静态成员 → 初始化必须带 template<class T>
template<class T>
ThreadPool<T> *ThreadPool<T>::_instance = nullptr;
template<class T>
Mutex ThreadPool<T>::_lock;
}
- 单例模式的核心实现规则:将类的构造函数私有化,从根源禁止外部直接创建对象;显式删除拷贝构造函数和赋值运算符重载,彻底杜绝通过拷贝、赋值生成新的实例;在此基础上,提供一个公有的静态成员函数作为全局唯一访问接口,该接口负责创建(懒汉 / 饿汉模式)并返回全局唯一的单例对象,最终保证程序整个生命周期中,该类有且仅有一个实例。
今天的分享就到此结束啦,如果对读者朋友们有所帮助的话,可否留下宝贵的三连呢~~
让我们共同努力, 一起走下去!