目录
- 一、准备工作
- 二、日志
- 三、线程池
-
- 3.1、线程池设计
- 3.2、线程安全的单例模式
-
- [3.2.1 饿汉模式](#3.2.1 饿汉模式)
- [3.2.2 懒汉模式](#3.2.2 懒汉模式)
- 3.3、完整代码及效果演示
一、准备工作
线程封装
锁封装
条件变量封装
二、日志
2.1、策略模式
设计模式: 是前人总结的"最佳实践模板",解决面向对象设计中反复出现的特定问题,让代码更灵活、可维护、可复用。
共有 23 种经典设计模式,策略模式就属于其中一种。
策略模式: 即定义一系列算法,把它们一个个封装起来,并且使它们可以互相替换。C++中可以利用多态实现策略模式:
cpp
// 策略接口(基类)
class PaymentStrategy {
public:
virtual void pay(int amount) = 0; // 纯虚函数
virtual ~PaymentStrategy() = default;
};
// 具体策略:支付宝
class Alipay : public PaymentStrategy {
public:
virtual void pay(int amount) override {
cout << "支付宝支付 " << amount << " 元" << endl;
}
};
// 具体策略:微信支付
class WeChatPay : public PaymentStrategy {
public:
virtual void pay(int amount) override {
cout << "微信支付 " << amount << " 元" << endl;
}
};
// 上下文:购物车
class ShoppingCart {
PaymentStrategy* strategy; // 持有策略(基类指针)
public:
void setStrategy(PaymentStrategy* s) { strategy = s; }
void checkout(int amount) {
strategy->pay(amount); // 调用当前策略(多态)
}
};
// 使用
int main() {
ShoppingCart cart;
cart.setStrategy(new Alipay());
cart.checkout(100); // 支付宝支付 100 元
cart.setStrategy(new WeChatPay());
cart.checkout(200); // 微信支付 200 元
}
未来我们的日志无非就是打印到显示器上,或者写入文件。所以,就可以实现向显示器打印和向文件写入两个子类。
2.2、日志设计
计算机中的日志是记录系统和软件运行中发生事件的文件,主要作用是监控行状态、记录异常信息,帮助快速定位问题并支持程序员进行问题修复。它是系统维护、故障排查和安全管理的重要工具。
日志应当有以下几部分组成:
- 时间戳
- 日志等级
- 日志内容
以下几个部分可选:
- 进程,线程id
- 文件名
- 行号
日志等级:
| 等级 | 含义 |
|---|---|
| DEBUG | 调试信息 |
| INFO | 正常运行信息 |
| WARN | 警告,非致命 |
| ERROR | 错误,功能受损 |
| FATAL | 致命,系统崩溃 |
我们打算设计的日志格式如下:
plain
[可读性很好的时间] [日志等级] [进程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
2.3、日志实现
2.3.1 策略模式实现日志显示
策略接口(基类):
cpp
class LogStrategy {
public:
virtual void SyncLog(const std::string message) = 0; // 纯虚函数
~LogStrategy() = default;
};
具体策略: 向显示器打印
**注意:**显示器属于临界资源,为了避免打印出现数据错乱,需要加锁。
cpp
class ConsoleLogStrategy : public LogStrategy {
public:
virtual void SyncLog(const std::string message) override
{
Mutex_RAII guard(_mutex); // 加锁
std::cout << message << "\r\n";
}
~ConsoleLogStrategy() {}
private:
Mutex _mutex;
};
具体策略: 向文件打印
- 细节一:我们需要一个已经存在文件路径,自己起一个文件名。那么就需要我们检查文件路径,可以用
std::filesystem中的exists方法检查,std::filesystem是C++17标准库,统一了跨平台的文件系统操作。 - 细节二:我们向文件写入日志,应该以追加方式写入。可以用
std::ofstream方法以追加方式打开一个文件,不存在则创建。 - 细节三:
std::ofstream打开文件,我们可以以追加重定向的方式向文件写入日志。 - 细节四:文件同样是共享资源,凡是涉及访问共享资源可能存在问题,我们必须加锁。
cpp
std::string gdefaultPath = "./"; // 默认当前路径
std::string gdefaultName = "log.log"; // 默认文件名
class FileLogStrategy : public LogStrategy {
public:
FileLogStrategy(std::string path = gdefaultPath, std::string name = gdefaultName)
: _path(path), _name(name)
{
// 对路径做检查
Mutex_RAII guard(_mutex); // 加锁
if (std::filesystem::exists(_path))
{
return;
}
try
{
std::filesystem::create_directories(_path);
}
catch (const std::filesystem::filesystem_error &err)
{
std::cout << err.what() << std::endl;
}
}
virtual void SyncLog(const std::string message) override
{
Mutex_RAII guard(_mutex); // 加锁
// 构造文件完整路径
std::string filename = _path + (_path.back() == '/' ? "" : "/") + _name;
// 以追加方式打开文件,不存在创建
std::ofstream out(filename, std::ios::app);
if (!out)
{
return;
}
out << message << "\r\n";
out.close();
}
~FileLogStrategy() {}
private:
std::string _path; // 文件路径
std::string _name; // 文件名
Mutex _mutex; // 锁
};
2.3.2 日志等级处理
日志等级我们用应该联合体表示,在联合体中日志等级其实就是一个整数,而我们在拼接日志时,希望以字符串形式显示日志等级,所以,我们需要做一下转换。
cpp
// 形成日志等级
enum class LogLevel
{
DEBUG,
INFO,
WARNING,
ERROR,
FATAL
};
// 将日志等级转化为字符串形式,LogLevel对象实际是一个整数
std::string Level2Str(LogLevel level)
{
switch (level)
{
case LogLevel::DEBUG:
return "DEBUG";
case LogLevel::INFO:
return "INFO";
case LogLevel::WARNING:
return "WARNING";
case LogLevel::ERROR:
return "ERROR";
case LogLevel::FATAL:
return "FATAL";
default:
return "UNKOWN";
}
}
2.3.3 时间戳处理
我们想要一个可读性很好的日志,如,2024-08-04 12:27:03,可以用snprintf进行格式控制。
获取当前时间戳:time,localtime_r可以将时间戳转化为一个结构体。通过这个结构体就可以获得年月日等等。

cpp
// 获取时间
std::string Time()
{
time_t now = time(nullptr); // 获取当前时间戳
tm tm;
localtime_r(&now, &tm);
char buf[128];
// 格式控制
snprintf(buf, sizeof(buf), "%4d-%02d-%02d %02d:%02d:%02d",
tm.tm_year + 1900,
tm.tm_mon + 1,
tm.tm_mday,
tm.tm_hour,
tm.tm_min,
tm.tm_sec);
return buf;
}
2.3.4 形成一条日志
- 细节一:我们采用在
Logger类中在实现一个LogMessage类。Logger类决定最终调用哪个策略,向显示器打印,或向文件打印。LogMessage类负责按照格式将所有数据形成日志。
为什么要在类中实现类?
这样LogMessage类形成日志之后就可以调用Logger的方法,显示日志。在LogMessage对象析构时,我们将日志打印出来,由指针的类型最终决定调用哪个方法(多态)。
- 细节二:
Logger类中可以用智能指针,基类函数指针类型,来帮我们调用策略函数,同时,主动内存管理。 - 细节三:形成一条日志,可以用
std::stringstream实现字符串和数据的流式转换,用重定向的方式将任意类型全部转化为字符串,非常方便。 - 细节四:未来我们的日志输出形式:
Logger() << "xxx" << xxx。所以,就需要重载<<操作符。同时,可能连续向显示器或文件打印 <<, 所以,为了支持这种写法,可以返回this指针指向的对象。其次,类型不同,我们可以先将其全部转化为字符串类型。 - 细节五:未来日志等级,打印日志的文件名,行号需要我们自己决定。但在
C/C++中有两个预定义宏:__FILE__和__LINE__,由编译器在预处理阶段自动替换为当前文件名和行号。所以,我们可以再处理一下,将logger(level, filename, line)定义为一个宏Log(levle),未来直接Log(level) << "xxx" << xxx调用即可。
如何实现???
由于LogMessage类在Logger中,必须在Logger类中实例化LogMessage类对象,就需要重载(),当我们调用
Log(levle),编译时替换为logger(level, filename, line),在重载函数内部LogMessage(level, file, line, *this)实例化LogMessage 对象。
cpp
class Logger
{
public:
Logger()
{
Enable_ConsoleLogStrategy();
}
// 创建实例化智能指针对象
void Enable_ConsoleLogStrategy()
{
_fllush_strategy = std::make_unique<ConsoleLogStrategy>();
}
void Enable_FileLogStrategy()
{
_fllush_strategy = std::make_unique<FileLogStrategy>();
}
// 形成一条日志
// 一条日志的形式:[时间] [日志等级] [pid] [文件名] [行数] - "内容"
// [2024-08-04 12:27:03] [DEBUG] [202938] [main.cc] [16] - hello world
class LogMessage
{
public:
LogMessage(LogLevel &level, const std::string &file, size_t line, Logger &logger)
: _cur_time(Time()),
_loglevel(Level2Str(level)),
_pid(getpid()),
_file(file),
_line_num(line),
_logger(logger)
{
// 日志左半部分
std::stringstream ss;
ss << "[" << _cur_time << "] ["
<< _loglevel << "] ["
<< _pid << "] ["
<< _file << "] ["
<< _line_num << "]"
<< " - ";
_log = ss.str();
}
// 日志输出形式: Logger() << "xxx" << xxx;
// 重载 <<
template <typename T>
LogMessage &operator<<(const T &info)
{
// 字符串流
std::stringstream ss;
ss << info;
_log += ss.str();
return *this; // 返回LogMessage对象,处理连续的<<,如: << "xxx" << 123 << 'a'
}
// 析构
~LogMessage()
{
// 调用日志显示方法(多态)
if (_logger._fllush_strategy)
{
// 由该智能指针类型决定调用哪个方法
_logger._fllush_strategy->SyncLog(_log);
}
}
private:
std::string _cur_time; // 时间
std::string _loglevel; // 日志等级
pid_t _pid; // 进程id
std::string _file; // 文件名
size_t _line_num; // 行号
std::string _log; // 整条日志
Logger &_logger; // 外部类对象,内部类用来调用日志显示方法
};
// 重载(),在内部类实例化Logger对象
LogMessage operator()(LogLevel level, const std::string file, size_t line)
{
return LogMessage(level, file, line, *this);
}
~Logger() {}
private:
std::unique_ptr<LogStrategy> _fllush_strategy; // 日志显示方法(智能指针)
};
Logger logger;
#define Log(level) logger(level, __FILE__, __LINE__)
2.3.5 完整代码及效果演示
日志测试:main.cc
cpp
#include "Log.hpp"
using namespace LogModuel;
int main()
{
logger.Enable_ConsoleLogStrategy(); // 向显示器打印
logger(LogLevel::DEBUG, "main.cc", 9) << "hello" << 123;
logger(LogLevel::DEBUG, "main.cc", 9) << "xxxxx" << 123;
logger(LogLevel::DEBUG, "main.cc", 9) << "aaaaa" << 123;
logger.Enable_FileLogStrategy(); // 向文件打印
logger(LogLevel::INFO, "main.cc", 9) << "hello" << 123;
logger(LogLevel::INFO, "main.cc", 9) << "xxxxx" << 123;
logger(LogLevel::INFO, "main.cc", 9) << "aaaaa" << 123;
return 0;
}

三、线程池
3.1、线程池设计
线程池:
一种线程使用模式。线程过多会带来调度开销,进而影响缓存局部性和整体性能。而线程池维护着多个线程,等待着监督管理者分配可并发执行的任务。这避免了在处理短时间任务时创建与销毁线程的代价。
线程池不仅能够保证内核的充分利用,还能防止过分调度。可用线程数量应该取决于可用的并发处理器、处理器内核、内存、网络sockets等的数量。
线程池的应用场景:
- 需要大量的线程来完成任务,且完成任务的时间比较短。
- 对性能要求苛刻的应用,比如要求服务器迅速响应客户请求。
- 接受突发性的大量请求,但不至于使服务器因此产生大量线程的应用。
即我们先创建一批线程,用特定的数据结构维护起来。没有任务时,线程在对应的条件变量下阻塞等待,有任务时,唤醒线程来处理。
流程:
- 启动线程池:创建线程并维护
- 向线程池排放任务
- 唤醒线程处理任务
- 关闭线程池,回收线程
完整代码及效果演示:
线程池测试: main.cc
cpp
#include "Log.hpp"
#include "PthreadPool.hpp"
#include "Task.hpp"
using namespace PthreadPoolMoudel;
using namespace LogModuel;
int main()
{
Logger logger;
logger.Enable_ConsoleLogStrategy(); // 日志打印到显示器
PthreadPool<task_t> *pool = new PthreadPool<task_t>();
pool->Start(); // 启动线程池
sleep(3);
int cnt = 10;
while(cnt)
{
pool->Equeue(Download);
sleep(1);
cnt--;
}
pool->Stop(); // 关闭线程池
pool->Join(); // 回收线程
return 0;
}

3.2、线程安全的单例模式
单例,即只有一个实例化的对象。
在很多服务器开发场景中,经常需要让服务器加载很多的数据 (上百G) 到内存中,为了节省空间,此时往往要用一个单例的类来管理这些数据。
单例模式有两种经典的实现模式:饿汉模式与懒汉模式。
举个例子:洗碗
- 吃完饭,立刻洗碗,这种就是饿汉方式。因为下一顿吃的时候可以立刻拿着碗就能吃饭。
- 吃完饭,先把碗放下,然后下一顿饭用到这个碗了再洗碗,就是懒汉方式。
懒汉方式最核心的思想是 "延时加载"。从而能够优化服务器的启动速度,这不难理解。在操作系统的设计哲学中,绝大多数场景都采用这种方法,比如,我们申请空间,实例化对象等等。
3.2.1 饿汉模式
cpp
class EagerSingleton
{
public:
static EagerSingleton &getInstance()
{
return _instance;
}
private:
EagerSingleton() = default; // 构造函数什么都不干
// 禁止拷贝
EagerSingleton(const EagerSingleton&) = delete;
EagerSingleton& operator=(const EagerSingleton&) = delete;
static EagerSingleton _instance; // 程序启动立即实例化对象
};
3.2.2 懒汉模式
cpp
class LazySingleton
{
public:
static LazySingleton *GetInstance()
{
// 需要的时候创建
if (inst == NULL)
{
inst = new LazySingleton();
}
return inst;
}
private:
LazySingleton() = default; // 构造函数什么都不干
// 禁止拷贝
LazySingleton(const LazySingleton&) = delete;
LazySingleton& operator=(const LazySingleton&) = delete;
static LazySingleton *inst;
};
3.3、完整代码及效果演示
我们以懒汉模式实现:
测试: main.cc
cpp
#include "Log.hpp"
#include "PthreadPool.hpp"
#include "Task.hpp"
using namespace PthreadPoolMoudel;
using namespace LogModuel;
int main()
{
Logger logger;
logger.Enable_ConsoleLogStrategy(); // 日志打印到显示器
sleep(3);
int cnt = 10;
while(cnt)
{
// 使用时创建单例,但只有第一次才创建对象
PthreadPool<task_t>::GetInstance()->Equeue(Download);
sleep(1);
cnt--;
}
PthreadPool<task_t>::GetInstance()->Stop(); // 关闭线程池
PthreadPool<task_t>::GetInstance()->Join(); // 回收线程
return 0;
}

线程池项目完整代码在这里:
今天的分享到此结束,如果感觉还不错点个赞支持一下吧,下次再见!!!