C++ - 基于多设计模式下的同步&异步日志系统
1. 项目介绍
本项目主要实现一个日志系统,其主要支持以下功能:
- 支持多级别日志消息
- 支持同步和异步日志
- 支持可靠写入日志到控制台、文件以及滚动文件中
- 支持多线程程序并发写日志
- 支持扩展不同的日志落地目的地
2. 开发环境
-
Ubuntu
-
vscode
-
g++/gdb
-
Makefile
3. 核心技术
- 类层次设计(继承和多态应用)
- C++11(多线程、auto、智能指针、右值引用等)
- 双缓冲区
- 生产消费模型
- 多线程
- 设计模式
4. 环境搭建
本项目不依赖任何第三方库,只需要有CentOS/Ubuntu+vscode/vim环境即可。
5. 日志系统介绍
5.1 为什么需要日志系统
- 生产环境的产品为了保证其稳定性及安全性,是不允许开发人员附加调试器去排查问题的,这就可以借助日志系统来打印一些日志帮助开发人员解决问题。
- 上线客户端的产品出现bug无法复现并解决,可以接入日志系统打印日志并上传到如服务器帮助开发人员进行分析。
- 对于一些高频操作(如定时器、心跳包)在少量调试次数下可能无法触发我们想要的行为,通过断点的暂停方式,我们可能需要重复几十上百次甚至更多,导致排查问题效率非常低下,这时可以借助打印日志的方式来排查问题。
- 在分布式、多线程/多进程代码中,出现bug比较难以定位,可以借助日志系统打印log帮助定位bug
- 帮助首次接触项目代码的新开发人员理解代码的运行流程
5.2 日志系统技术实现
日志系统的技术实现主要包括三种类型:
- 在我们学习过程中,利用printf、std::cout等输出函数将日志信息打印到控制台
- 对于大型商业化项目,为了方便排查问题,我们一般会将日志输出到文件或者数据库系统方便查询和分析日志,主要分为同步日志和异步日志方式
- 同步写日志
- 异步写日志
5.2.1 同步写日志
同步写日志是指当输出日志时,必须等待日志输出语句执行完毕后,才能执行后面的业务逻辑语句,也就是说,日志逻辑和业务逻辑是在同一个线程运行。

在高并发场景下,随着日志数量不断增加,同步日志系统容易产生瓶颈:
- 一方面,大量的日志打印陷入等量的write系统调用,有一定的系统开销。
- 另一方面,打印日志的进程附带了大量同步的磁盘IO,影响程序性能
5.2.2 异步写日志
异步日志是指在进行日志输出是,日志输出语句与业务逻辑语句并非在一个线程中运行,而是有专门的线程用于进行日志输出的操作。
- 业务线程只需要将日志放到一个内存缓冲区中不用等待即可继续执行后续的业务逻辑(日志生产者)。
- 日志线程完成日志的落地操作(日志消费者)。
这是一个经典的生产-消费者模型。

这样做的好处是,即使日志还没写完,也不影响主业务运行,可以提高程序的性能:
- 主线程调用人日志打印成为非阻塞接口。
- 同步的磁盘IO由主线程中剥离出来交给单独的线程完成。
6. 相关技术补充
6.1 不定宏参数
如同printf一样,我们使用printf进行打印到时候,可以输入数量不定的参数,在函数内部可以根据格式化字符串中格式化字符串分别获取不同的参数进行数据的格式化。
6.1.1 不定参宏函数
cpp
#include <iostream>
#include <cstdarg>
#define LOG(fmt, ...) printf("[%s:%d] " fmt "\n", __FILE__, __LINE__, ##__VA_ARGS__)
int main()
{
LOG("%s-%s", "hello", "world");
return 0;
}
6.1.2 C风格不定参函数
cpp
#include <iostream>
#include <cstdarg>
void printNum(int n, ...)
{
va_list al;
va_start(al, n); // 让al指向指定参数之后的第一个可变参数
for (int i = 0; i < n; i++)
{
int num = va_arg(al, int); // 按指定类型取出al指向的参数,al向后移动一步
printf("%d\n", num);
}
va_end(al); // 清空可变参数列表-实际是将al置空
}
int main()
{
printNum(5, 1, 2, 3, 4, 5);
return 0;
}
cpp
#include <iostream>
#include <cstdarg>
void myprintf(const char* fmt, ...)
{
char* res;
va_list al;
va_start(al, fmt);
int len = vasprintf(&res, fmt, al);
va_end(al);
std::cout << res << std::endl;
free(res);
}
int main()
{
myprintf("%s-%d", "小明", 18);
return 0;
}
6.1.3 C++风格不定参函数
cpp
#include <iostream>
void xprintf()
{
std::cout << std::endl;
}
template<typename T, typename ...Args>
void xprintf(const T& value, Args&& ...args)
{
std::cout << value << " ";
if ((sizeof ...(args)) > 0) xprintf(std::forward<Args>(args)...);
else xprintf();
}
int main()
{
xprintf("hello");
xprintf("hello", 666);
xprintf("hello", "world", 666);
return 0;
}
6.2 设计模式
设计模式是前辈对代码开发经验的总结,是解决特定问题的一系列套路,不是语法规定,而是一套用来提高代码复用性、可维护性、可读性、稳健性及安全性的解决方案。
6.2.1 六大原则
-
单一职责原则(Single Responsibility Principle)
- 类的职责应该单一,一个方法只做一件事,职责划分清晰,每次改动到最小单位的方法或类。
- 两个完全不相干的功能不应该放在一个类中,一个类中应该是一组相关性很高的函数。
-
开闭原则(Open Closed Principle)
- 对扩展开放,对修改封闭。
- 对软件实体的改动,最好使用扩展而非修改的方式。
- 例如:超市卖货,促销商品的价格不修改原来的价格,而是在那基础之上新增促销价格。
-
里氏替换原则(Liskov Substitution Principle)
- 通俗的说,只要父类能出现的地方,子类就可以出现,而且替换为子类也不会产生任何错误或者异常。
- 在继承类的时候,务必重写父类的所有方法,尤其注意父类的protected方法,子类尽量不要暴露自己的public方法供外界调用。
- 子类必须完全实现父类的方法,子类可以有自己的个性,覆盖或实现父类的方法是,输入参数可以被放大,输出可以被缩小。
-
依赖倒置原则(Dependence Inversion Principle)
- 高层模块不应当依赖底层模块,两者都应该依赖其抽象。
- 不可分割的原子逻辑就是底层模块,原子逻辑组装成的就是高层模块。
- 模块间通过抽象(接口)发生,具体类之间不直接进行依赖。
- 每个类都尽量有抽象类,任何类都不应当从具体类派生,尽量不要重写基类的方法,结合里氏替换原则使用。
-
迪米特法则(Law of Demeter),又称"最少知道法则"
- 尽量减少对象之间的交互,从而减小类之间的耦合。一个对象应该对其他对象由最少的了解。
-
接口隔离原则(Interface Segregation Principle)
- 客户端不应该依赖它不需要的接口,类简单依赖关系应该建立在最小接口上。
- 接口设计精良精简单一,不要对外暴露没有实际意义的接口。
从整体理解六大设计原则:用抽象构建框架,用实现扩展细节,具体到每一条设计都对应着一条注意事项:
| 原则 | 注意事项 |
|---|---|
| 单一职责原则 | 实现类要职责单一 |
| 里氏替换原则 | 不要破坏继承体系 |
| 依赖倒置原则 | 面向接口编程 |
| 接口隔离原则 | 设计接口要将精简单一 |
| 迪米特法则 | 降低耦合 |
| 开闭原则 | 对扩展开放,对修改关闭 |
6.2.2 单例模式
一个类只能创建一个对象,即单例模式。
该设计模式保证系统中有且仅有一个实例,并提供一个全局访问点,该实例被所有程序模块共享。
比如在某个服务器中,该服务器配置信息文件由一个单例对象读取,然后其他对象通过这个单例对象来获取这些配置信息,这种凡是简化了在复杂环境下的配置管理。
饿汉模式:程序启动时就创建单例。
cpp
#include <iostream>
class Singleton
{
private:
static Singleton _eton;
private:
Singleton() {}
~Singleton() {}
public:
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
static Singleton& getInstance() { return _eton; }
};
Singleton Singleton::_eton;
懒汉模式:在第一次被调用时才创建单例。
cpp
class Singleton
{
private:
Singleton() {}
~Singleton() {}
public:
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
static Singleton& getInstance()
{
static Singleton _eton;
return _eton;
}
};
6.2.3 工厂模式
工厂模式是一种创建型设计模式,它提供了一种创造对象的最佳方式。在工厂中,我们创建对象时不会对上层暴露创建逻辑,而是使用一个共同结构来只想新创建的对象,以此实现创建和使用的分离。
工厂模式可以分为简单工厂模式、工厂方法模式、抽象工厂模式。
简单工厂模式
简单工厂模式由一个工厂对象通过类型决定创建出来指定产品类的实例。
假设有一个工厂能生产出水果,当客户需要产品的时候明确告知工厂生产哪类水果,工厂需要接收用户提供的类别信息,当新增产品的时候,工厂内部去添加新产品的生产方式。
cpp
#include <iostream>
#include <string>
#include <memory>
class Fruit
{
public:
Fruit() {}
virtual void show() = 0;
};
class Apple : public Fruit
{
public:
Apple() {}
virtual void show() {
std::cout << "我是一个苹果" << std::endl;
}
};
class Banana : public Fruit
{
public:
Banana() {}
virtual void show() {
std::cout << "我是一个香蕉" << std::endl;
}
};
// 工厂类,通过参数控制就可以生成任何产品
class FruitFactory
{
public:
static std::shared_ptr<Fruit> create(const std::string& name) {
if (name == "苹果") {
return std::make_shared<Apple>();
}
else if (name == "香蕉") {
return std::make_shared<Banana>();
}
return std::shared_ptr<Fruit>();
}
};
int main()
{
std::shared_ptr<Fruit> fruit = FruitFactory::create("苹果");
fruit->show();
fruit = FruitFactory::create("香蕉");
fruit->show();
return 0;
}
这个模式的结构和管理产品对象的方式非常简单。
但是它的扩展性太差,如果需要新增产品,就必须修改工厂类,新增产品的创建逻辑,这就违反了开闭原则。
工厂方法模式
在简单工厂模式下新增多个工厂,多个产品,每个产品对应一个工厂。
工厂不需要再接收客户的产品类别,只负责生产产品。
cpp
#include <iostream>
#include <string>
#include <memory>
class Fruit
{
public:
Fruit() {}
virtual void show() = 0;
};
class Apple : public Fruit
{
public:
Apple() {}
virtual void show()
{
std::cout << "我是一个苹果" << std::endl;
}
private:
std::string _color;
};
class Banana : public Fruit
{
public:
Banana() {}
virtual void show() {
std::cout << "我是一个香蕉" << std::endl;
}
};
class FruitFactory
{
public:
virtual std::shared_ptr<Fruit> create() = 0;
};
class AppleFactory : public FruitFactory
{
public:
virtual std::shared_ptr<Fruit> create()
{
return std::make_shared<Apple>();
}
};
class BananaFactory : public FruitFactory
{
public:
virtual std::shared_ptr<Fruit> create()
{
return std::make_shared<Banana>();
}
};
int main()
{
std::shared_ptr<FruitFactory> factory(new AppleFactory());
std::shared_ptr<Fruit> fruit = factory->create();
fruit->show();
factory.reset(new BananaFactory());
fruit = factory->create();
fruit->show();
return 0;
}
这样就减轻了工厂类的负担,每个工厂都只负责生产指定的产品。此时新增新的产品就只需要新增新的工厂,而不需要修改原来的工厂,这样就遵循了开闭原则。
缺点在于,对于某种可以形成一组产品族的清空处理较为复杂,每增加一个产品都需要新增工厂类,这样就需要创建大量的工厂类,在一定程度上增加了系统的耦合度。
抽象工厂模式
在工厂方法模式的基础上,我们将一些相关的产品组成一个产品族,由同一个共产来生产。
cpp
#include <iostream>
#include <string>
#include <memory>
class Fruit
{
public:
Fruit() {}
virtual void show() = 0;
};
class Apple : public Fruit
{
public:
Apple() {}
virtual void show()
{
std::cout << "我是一个苹果" << std::endl;
}
private:
std::string _color;
};
class Banana : public Fruit
{
public:
Banana() {}
virtual void show()
{
std::cout << "我是一个香蕉" << std::endl;
}
};
class Animal
{
public:
virtual void voice() = 0;
};
class Lamp : public Animal
{
public:
void voice() { std::cout << "咩咩咩\n"; }
};
class Dog : public Animal
{
public:
void voice() { std::cout << "汪汪汪\n"; }
};
class Factory
{
public:
virtual std::shared_ptr<Fruit> getFruit(const std::string& name) = 0;
virtual std::shared_ptr<Animal> getAnimal(const std::string& name) = 0;
};
class FruitFactory : public Factory
{
public:
virtual std::shared_ptr<Animal> getAnimal(const std::string& name)
{
return std::shared_ptr<Animal>();
}
virtual std::shared_ptr<Fruit> getFruit(const std::string& name)
{
if (name == "苹果")
{
return std::make_shared<Apple>();
}
else if (name == "香蕉")
{
return std::make_shared<Banana>();
}
return std::shared_ptr<Fruit>();
}
};
class AnimalFactory : public Factory {
public:
virtual std::shared_ptr<Fruit> getFruit(const std::string& name) {
return std::shared_ptr<Fruit>();
}
virtual std::shared_ptr<Animal> getAnimal(const std::string& name) {
if (name == "小羊")
{
return std::make_shared<Lamp>();
}
else if (name == "小狗")
{
return std::make_shared<Dog>();
}
return std::shared_ptr<Animal>();
}
};
class FactoryProducer {
public:
static std::shared_ptr<Factory> getFactory(const std::string& name) {
if (name == "动物")
{
return std::make_shared<AnimalFactory>();
}
else
{
return std::make_shared<FruitFactory>();
}
}
};
int main()
{
std::shared_ptr<Factory> fruit_factory = FactoryProducer::getFactory("水果");
std::shared_ptr<Fruit> fruit = fruit_factory->getFruit("苹果");
fruit->show();
fruit = fruit_factory->getFruit("香蕉");
fruit->show();
std::shared_ptr<Factory> animal_factory = FactoryProducer::getFactory("动物");
std::shared_ptr<Animal> animal = animal_factory->getAnimal("小羊");
animal->voice();
animal = animal_factory->getAnimal("小狗");
animal->voice();
return 0;
}
围绕着一个超级工厂创建其他工厂,每个生成的共产按照工厂模式提供对象。
组要思想:将工厂抽象成两层,抽象工厂和具体工厂子类,在工厂子类中生产不同类型的子产品。
抽象工厂模式适用于生产多个工厂系列产品衍生的设计模式,增加新的产品等级结构复杂,需要对原有系统进行较大的修改,甚至需要修改抽象层代码,违背了开闭原则。
6.2.4 建造者模式
建造者模式时一种创建型设计模式,使用多个简单的对象一步一步构建成一个复杂对象,能够将一个复杂的对象的构建与它的表现分离,提供一种创建对象的最佳方式。
主要用于解决对象的构建过于复杂的问题。
建造者模式主要基于四个核心类实现:
- 抽象产品类:抽象的产品对象类,作为具体产品类的基类。
- 具体产品类:具体的产品对象类。
- 抽象Builder类:创建一个产品对象所需的各个部件的抽象接口。
- 具体产品的Builder类:实现抽象接口,构建各个部件。
- 指挥者Director类:统一组建过程,提供给给调用者使用,通过指挥者来构建产品。
cpp
#include <iostream>
#include <memory>
/*抽象电脑类*/
class Computer
{
public:
using ptr = std::shared_ptr<Computer>;
Computer() {}
void setBoard(const std::string& board) { _board = board; }
void setDisplay(const std::string& display) { _display = display; }
virtual void setOs() = 0;
std::string toString()
{
std::string computer = "Computer:{\n";
computer += "\tboard=" + _board + ",\n";
computer += "\tdisplay=" + _display + ",\n";
computer += "\tOs=" + _os + ",\n";
computer += "}\n";
return computer;
}
protected:
std::string _board;
std::string _display;
std::string _os;
};
/*具体产品类*/
class MacBook : public Computer
{
public:
using ptr = std::shared_ptr<MacBook>;
MacBook() {}
virtual void setOs()
{
_os = "Max Os X12";
}
};
/*抽象建造者类:包含创建一个产品对象的各个部件的抽象接口*/
class Builder
{
public:
using ptr = std::shared_ptr<Builder>;
virtual void buildBoard(const std::string& board) = 0;
virtual void buildDisplay(const std::string& display) = 0;
virtual void buildOs() = 0;
virtual Computer::ptr build() = 0;
};
/*具体产品的具体建造者类:实现抽象接口,构建和组装各个部件*/
class MacBookBuilder : public Builder {
public:
using ptr = std::shared_ptr<MacBookBuilder>;
MacBookBuilder() : _computer(new MacBook()) {}
virtual void buildBoard(const std::string& board)
{
_computer->setBoard(board);
}
virtual void buildDisplay(const std::string& display)
{
_computer->setDisplay(display);
}
virtual void buildOs()
{
_computer->setOs();
}
virtual Computer::ptr build()
{
return _computer;
}
private:
Computer::ptr _computer;
};
/*指挥者类,提供给调用者使用,通过指挥者来构建复杂产品*/
class Director
{
public:
Director(Builder* builder) :_builder(builder) {}
void construct(const std::string& board, const std::string& display)
{
_builder->buildBoard(board);
_builder->buildDisplay(display);
_builder->buildOs();
}
private:
Builder::ptr _builder;
};
int main()
{
Builder* buidler = new MacBookBuilder();
std::unique_ptr<Director> pd(new Director(buidler));
pd->construct("英特尔主板", "VOC显示器");
Computer::ptr computer = buidler->build();
std::cout << computer->toString();
return 0;
}
6.2.5 代理模式
代理模式:代理控制对其他对象的访问,也就是代理对象控制对原对象的引用。
在某些情况下,一个对象不适合或者不能被引用访问,而代理对象可以在客户端和目标对象之间起到中介的作用。
代理模式的结构包括:一个真正需要访问的类(目标类)、一个代理对象。
目标对象和代理对象实现公益岗接口,先访问代理类在通过代理类访问目标对象。
代理模式分为静态代理和动态代理:
- 静态代理:在编译时就确定了代理类和被代理类的关系。
- 动态代理:在运行时才动态生成代理类,并与被代理类绑定。
cpp
#include <iostream>
#include <string>
class RentHouse
{
public:
virtual void rentHouse() = 0;
};
/*房东类:将房子租出去*/
class Landlord : public RentHouse
{
public:
void rentHouse()
{
std::cout << "将房子租出去\n";
}
};
/*中介代理类:对租房子进行功能加强,实现租房以外的其他功能*/
class Intermediary : public RentHouse
{
public:
void rentHouse()
{
std::cout << "发布招租启示\n";
std::cout << "带人看房\n";
_landlord.rentHouse();
std::cout << "负责租后维修\n";
}
private:
Landlord _landlord;
};
int main()
{
Intermediary intermediary;
intermediary.rentHouse();
return 0;
}
7. 日志系统框架设计
本项目实现的是一个多日志器日志系统,主要实现的功能是让程序员能够轻松的将程序运行日志信息落地到指定的位置,且支持同步与异步两种方式的日志落地方式。
7.1 模块划分
日志等级模块
- UNKNOW:未知等级。
- DEBUG:调试,调试时的关键信息输出。
- INFO:提示,普通的提示型日志信息。
- WARNING:警告,不影响运行,但需要注意一下的日志。
- ERROR:错误,程序出现错误的日志。
- FATAL:致命, 一般是代码异常导致程序无法继续推进的日志。
- OFF:用于关闭日志输出
日志消息模块
- 时间:本条日志输出的时间
- 线程ID:描述本条日志是哪个线程输出的
- 日志等级:描述本条日志的等级
- 日志数据:本条日志的有效载荷数据
- 日志文件名:描述本条日志在哪个源码文件中输出的
- 日志行号:描述本条日志在源码文件中的哪一行输出。
日志消息格式化模块
我们希望格式化日志消息,由用户指定日志输出的格式(就像我们使用printf一样),我们先规定好占位符,然后由用户指定输出格式:
-
%d:日期时间,其中还可以指定日期时间的指定格式(根据strftime函数的格式化字符串指定,这里介绍常用的,有需要可以自行查询),跟在%d后面用花括号括起来:
- %Y:四位数年份
- %m:两位数月份
- %d:两位数日期
- %H:两位数24小时制数字
- %M:两位数分钟
- %S:两位数秒
-
%T:缩进
-
%t:线程id
-
%p:日志级别
-
%c:日志器名称
-
%f:文件名
-
%l:行号
-
%m:日志消息
-
%n:换行
-
%%:'%'字符
指定内容:
"[%d{%Y-%m-%d %H:%M:%S}][%t][%c][%f:%l][%p]%T%m%n", create socket error
输出内容:
[2026-03-10 12:31:48][13579][root][main.cc:36][FATAL] create socket error\n
设计思想:设计不同的子类,从不同子类中取出不同的数据进行处理
日志消息落地模块
决定了日志的落地方向,可以是标准输出,也可以是日志文件或滚动文件输出等。
- 标准输出:标准输出打印。
- 日志文件输出:将日志写入指定文件末尾。
- 滚动文件输出:以文件大小进行控制,当一个日志文件达到指定大小,则切换下一个文件。
- 支持扩展远程日志输出,创建客户端,将日志消息发送给远程的日志分析服务器。
设计思想:设计不同的子类,由不同子类控制不同日志落地方向。
日志器模块
对上面几个模块进行整合,用户通过日志器进行日志输出,降低用户使用难度。
日志器管理模块
为了降低项目开发的日志耦合,不同的项目组可以有自己的日志器来控制输出格式以及落地方向,本项目是一个多日志器的日志系统。
管理模块就是对所有日志器进行统一管理,并提供默认日志器,提供标准输出的日志输出。
异步线程模块
实现对日志的异步输出功能,用户只需要将输入日志放入任务池,异步线程负责日志的落地输出功能,一次提供更高效的非阻塞日志输出。
7.2 模块关系图

8. 代码设计
8.1 实用类设计
提前完成一些零碎的功能接口,以便于在项目中使用到。
- 获取当前系统时间
- 判断当前文件是否存在
- 获取文件所在目录的路径
- 创建目录
cpp
// util.hpp
/*
工具集模块:实现与业务无关的功能
1. 获取当前系统时间
2. 判断当前文件是否存在
3. 截取当前路径中的目录
4. 创建指定目录
*/
#ifndef __M_UTIL_H__
#define __M_UTIL_H__
#include <string>
#include <ctime>
#include <sys/stat.h>
namespace salog {
namespace util
{
class data {
public:
// 获取当前系统时间
static time_t now() { return time(nullptr); }
};
class file {
public:
// 判断当前目录是否存在
static bool exists(const std::string &name) {
struct stat st;
return stat(name.c_str(), &st) == 0;
}
// 截取当前路径中的目录
static std::string path(const std::string &name) {
if (name.empty()) return ".";
size_t pos = name.find_last_of("/\\");
if (pos == std::string::npos) return ".";
return name.substr(0, pos + 1);
}
// 创建指定目录
static void create_directory(const std::string &path) {
if (path.empty() || exists(path)) return;
size_t pos, idx = 0;
while (idx < path.size()) {
pos = path.find_first_of("/\\", idx);
if (pos == std::string::npos) { mkdir(path.c_str(), 0777); return; }
if (pos == idx) { idx = pos + 1; continue; }
std::string subdir = path.substr(0, pos);
if (subdir == "." || subdir == ".." || exists(subdir)) { idx = pos + 1; continue; }
mkdir(subdir.c_str(), 0777);
idx = pos + 1;
}
}
};
}
}
#endif
8.2 日志等级类设计
日志等级分为八个等级,上面已经详细介绍,这里不再赘述。
cpp
// level.hpp
/*
日志等级模块
1. 定义日志等级
2. 将日志等级转换为字符串
*/
#ifndef __M_LEVEL_H__
#define __M_LEVEL_H__
#include <string>
namespace salog {
class LogLevel {
public:
// 日志等级的定义
enum class value {
UNKNOW,
DEBUG,
INFO,
WARNING,
ERROR,
FATAL,
OFF
};
static const char *toString(LogLevel::value level) {
switch (level) {
// 字符串化
#define TOSTRING(name) #name
case LogLevel::value::DEBUG: return TOSTRING(DEBUG);
case LogLevel::value::INFO: return TOSTRING(INFO);
case LogLevel::value::WARNING: return TOSTRING(WARNING);
case LogLevel::value::ERROR: return TOSTRING(ERROR);
case LogLevel::value::FATAL: return TOSTRING(FATAL);
case LogLevel::value::OFF: return TOSTRING(OFF);
#undef TOSTRING
}
return "UNKNOW";
}
};
}
#endif
8.3 日志消息类设计
消息日志类主要是封装一条完整的日志消息所需的内容,其中包括日志等级、打印日志的源文件名、行号等信息。
cpp
// message.hpp
/*
消息类模块
消息类管理成员:
1. 时间
2. 文件名
3. 行号
4. 线程id
5. 日志器名称
6. 日志等级
7. 日志消息
*/
#ifndef __M_MSG_H__
#define __M_MSG_H__
#include <memory>
#include <thread>
#include <string>
#include "util.hpp"
#include "level.hpp"
namespace salog {
struct LogMsg
{
using ptr = std::shared_ptr<LogMsg>;
time_t _time;
std::string _file;
size_t _line;
std::thread::id _tid;
std::string _name;
LogLevel::value _level;
std::string _payload;
LogMsg(const std::string &file,
size_t line,
const std::string &name,
LogLevel::value level,
const std::string &&payload)
: _time(util::data::now())
, _file(file)
, _line(line)
, _tid(std::this_thread::get_id())
, _name(name)
, _level(level)
, _payload(payload) {}
};
}
#endif
8.4 日志输出格式化类设计
是指格式化(Formatter)类主要负责格式化日志消息,主要包括以下内容:
- pattern成员:保存日志的输出格式字符串。
- %d:日期时间,其中还可以指定日期时间的指定格式(根据strftime函数的格式化字符串指定,这里介绍常用的,有需要可以自行查询):
- %Y:四位数年份
- %m:两位数月份
- %d:两位数日期
- %H:两位数24小时制数字
- %M:两位数分钟
- %S:两位数秒
- %T:缩进
- %t:线程id
- %p:日志级别
- %c:日志器名称
- %f:文件名
- %l:行号
- %m:日志消息
- %n:换行
- %%:'%'字符
- %d:日期时间,其中还可以指定日期时间的指定格式(根据strftime函数的格式化字符串指定,这里介绍常用的,有需要可以自行查询):
- std::vector<FormatItem::ptr> items成员:用于按需保存格式化字符串对应的子格式化对象。
FormatItem类主要负责日志消息子项的获取及格式化。其包含以下子类:
- MsgFormatItem:负责从LogMsg中取出有效日志数据。
- LevelFormatItem:负责从LogMsg中取出日志等级。
- NameFormatItem:负责从LogMsg中取出日志器名称。
- ThreadFormatItem:负责从LogMsg中取出线程ID。
- TimeFormatItem:负责从LogMsg中取出时间戳并按指定格式进行格式化。
- FileFormatItem:负责从LogMsg中取出源码所在文件名。
- LineFormatItem:负责从LogMsg中取出源码所在行号。
- TabFormatItem:表示一个制表符缩进。
- NLineFormatItem:表示一个换行。
- OtherFormatItem:表示非格式化的原始字符串。
cpp
// format.hpp
/*
格式化模块
1. 消息提取抽象基类:
成员:消息提取纯虚函数,由派生类进行具体实现
2. 派生消息提取类:
1. 具体实现msg中各项成员的消息提取
3. 格式化类
成员变量:
1. std::string _pattern,存放外部提供的具体格式化字符串
2. std::vector<FormatItem::ptr> _items,根据格式化字符串按序将各消息提取类指针存放在该容器中
public成员函数:
1. 构造:提供默认格式化字符串,若外界没有传入格式化字符串,则按照默认格式化字符串进行处理
2. std::ostream& format(std::ostream &os, const LogMsg &msg):将传入的LogMsg格式化并输出到指定流
3. std::string format(const LogMsg &msg):将传入的LogMsg格式化为字符串输出
*/
#ifndef __M_FMT_H__
#define __M_FMT_H__
// 默认支持的最大格式化时间字符串长度
#define TIME_FORMAT_MAX_SIZE 128
#include <iostream>
#include <sstream>
#include <vector>
#include <cassert>
#include "message.hpp"
namespace salog {
class FormatItem {
public:
using ptr = std::shared_ptr<FormatItem>;
virtual ~FormatItem() {}
virtual void format(std::ostream &os, const LogMsg &msg) = 0;
};
class TimeFormatItem : public FormatItem {
public:
// 默认情况下如果不传递指定格式,就按"yyyy-mm-dd hh:mm:ss"进行格式化
TimeFormatItem(const std::string format = "%Y:%m:%d %H:%M:%S"): _format(format) {
if (format.empty()) _format = "%Y:%m:%d %H:%M:%S";
}
virtual void format(std::ostream &os, const LogMsg &msg) override {
time_t t = msg._time;
struct tm lt;
localtime_r(&t, <);
char tmp[128]; // 一般情况下,格式化时间长度不应当超过128
strftime(tmp, 127, _format.c_str(), <);
os << tmp;
}
private:
std::string _format;
};
class FileFormatItem : public FormatItem {
public:
virtual void format(std::ostream &os, const LogMsg &msg) override {
os << msg._file;
}
};
class LineFormatItem : public FormatItem {
public:
virtual void format(std::ostream &os, const LogMsg &msg) override {
os << msg._line;
}
};
class ThreadFormatItem : public FormatItem {
public:
virtual void format(std::ostream &os, const LogMsg &msg) override {
os << msg._tid;
}
};
class NameFormatItem : public FormatItem {
public:
virtual void format(std::ostream &os, const LogMsg &msg) override {
os << msg._name;
}
};
class LevelFormatItem : public FormatItem {
public:
virtual void format(std::ostream &os, const LogMsg &msg) override {
os << LogLevel::toString(msg._level);
}
};
class MsgFormatItem : public FormatItem {
public:
virtual void format(std::ostream &os, const LogMsg &msg) override {
os << msg._payload;
}
};
class TabFormatItem : public FormatItem {
public:
virtual void format(std::ostream &os, const LogMsg &msg) override {
os << "\t";
}
};
class NLineFormatItem : public FormatItem {
public:
virtual void format(std::ostream &os, const LogMsg &msg) override {
os << "\n";
}
};
class OtherFormatItem : public FormatItem {
public:
OtherFormatItem(const std::string &str = "") :_str(str) {}
virtual void format(std::ostream &os, const LogMsg &msg) override {
os << _str;
}
private:
std::string _str;
};
class Formatter {
public:
using ptr = std::shared_ptr<Formatter>;
/*
%d 日期
%T 缩进
%t 线程id
%p 日志级别
%c 日志器名称
%f 文件名
%l 行号
%m 日志消息
%n 换行
*/
Formatter(const std::string &pattern = "[%d{%Y:%m:%d %H:%M:%S}][%t][%f:%l][%c][%p]%T%m%n")
:_pattern(pattern) {
assert(parsePattern());
}
std::ostream& format(std::ostream &os, const LogMsg &msg) {
for (auto &item : _items) {
item->format(os, msg);
}
return os;
}
std::string format(const LogMsg &msg) {
std::stringstream ss;
format(ss, msg);
return ss.str();
}
private:
// 通过格式化字符返回对应消息提取对象
FormatItem::ptr createItem(const std::string &fc, const std::string &subfmt) {
// 只有d有子格式,其他格式化字符串的子格式将被忽略
if (fc == "d") return FormatItem::ptr(new TimeFormatItem(subfmt));
if (fc == "T") return FormatItem::ptr(new TabFormatItem());
if (fc == "t") return FormatItem::ptr(new ThreadFormatItem());
if (fc == "p") return FormatItem::ptr(new LevelFormatItem());
if (fc == "c") return FormatItem::ptr(new NameFormatItem());
if (fc == "f") return FormatItem::ptr(new FileFormatItem());
if (fc == "l") return FormatItem::ptr(new LineFormatItem());
if (fc == "m") return FormatItem::ptr(new MsgFormatItem());
if (fc == "n") return FormatItem::ptr(new NLineFormatItem());
return FormatItem::ptr();
}
// 解析格式化字符串
bool parsePattern() {
std::string format_key; // 格式化字符
std::string format_val; // 格式化字符后的子格式{}
std::string string_row; // 普通字符串
size_t pos = 0;
while (pos < _pattern.size()) {
// 普通字符处理
if (_pattern[pos] != '%') { string_row += _pattern[pos++]; continue; }
// %字符处理
if (pos + 1 < _pattern.size() && _pattern[pos + 1] == '%') { string_row += '%'; pos += 2; continue; }
// 先将前面累计的普通字符串添加到_items数组中
if (!string_row.empty()) {
_items.push_back(std::make_shared<OtherFormatItem>(string_row));
string_row.clear();
}
// 此时pos肯定指向%且后面位置不是%
pos++;
if (pos >= _pattern.size()) {
std::cerr << "格式化字符串不完整" << std::endl;
return false;
}
// 格式化字符肯定存在
format_key = _pattern[pos++];
// 判断后面是否包含子格式
bool sub_format_error = true;
if (pos < _pattern.size() && _pattern[pos] == '{') {
pos++;
while (pos < _pattern.size()) {
if (_pattern[pos] == '}') {
sub_format_error = false;
pos++;
break;
}
format_val += _pattern[pos++];
}
// 到结尾还没发现},出错
if (sub_format_error) {
std::cerr << "{}子格式匹配出错" << std::endl;
return false;
}
}
// 到这里已经拿到了对应的格式化字符和子格式,添加到_items
FormatItem::ptr item = createItem(format_key, format_val);
if (item.get() == nullptr) {
std::cerr << "没有对应的格式化字符:" << format_key << std::endl;
return false;
}
_items.push_back(item);
format_key.clear(); format_val.clear();
}
// 可能最后一部分是普通字符串,添加到_items
if (!string_row.empty()) {
_items.push_back(std::make_shared<OtherFormatItem>(string_row));
string_row.clear();
}
return true;
}
private:
std::string _pattern;
std::vector<FormatItem::ptr> _items;
};
}
#undef TIME_FORMAT_MAX_SIZE
#endif
8.5 日志落地类(LogSink)设计(使用工厂模式)
日志落地类主要负责将日志消息落地到指定目的地。
它主要包括以下内容:
- LogSink日志落地类:作为抽象类,其中包含log方法,设为纯虚函数,当需要进行扩展落地方式时,继承该类并重写log方法即可。
目前实现三个不同方向上的是指落地:
- StdoutSink:标准输出
- FileSink:指定文件输出
- RollBySizeSink:根据文件大小进行滚动文件输出
- RollByTimeSink:根据时间进行滚动文件输出
最后通过一个工厂类SinkFactory来调用上述的不同落地方式。
滚动日志文件输出的必要性:
- 由于及其磁盘空间有限,我们不可能一直无限地向一个文件中添加数据
- 如果一个日志文件体积太大,不仅不好打开,而且包含数据量巨大,不方便查找。
- 所以在实际开发中,对于单个日志文件的大小会做一些限制,即当大小超过指定限制(如1GB)或超过指定时间段(如1天),就创建新的文件来写入日志。对于过期的日志,定时进行清理。
cpp
// sink.hpp
/*
日志落地模块(使用工厂模式)
1. 日志落地抽象基类(抽象产品类)
成员函数:纯虚函数virtual void log(const char *data, size_t len) = 0,具体由子类实现
2. 各落地方向子类(产品类)
标准输出
指定文件
滚动文件-根据文件大小滚动
滚动文件-根据时间滚动
(支持扩展,只需要继承抽象产品类就可以自行引入新的落地方向)
3. 日志落地生产类(工厂类)
提供静态函数模板create,根据输入的类型获取对应的落地方向对象。
*/
#ifndef __M_SINK_H__
#define __M_SINK_H__
#include "util.hpp"
#include <iostream>
#include <fstream>
#include <memory>
#include <cassert>
namespace salog {
class LogSink {
public:
using ptr = std::shared_ptr<LogSink>;
virtual ~LogSink() {}
virtual void log(const char *data, size_t len) = 0;
};
class StdoutSink : public LogSink {
public:
virtual void log(const char *data, size_t len) override {
std::cout.write(data, len);
}
};
class FileSink : public LogSink {
public:
FileSink(const std::string &filename) {
util::file::create_directory(util::file::path(filename));
_ofs.open(filename, std::ios::binary | std::ios::app);
assert(_ofs.is_open());
}
virtual void log(const char *data, size_t len) override {
_ofs.write(data, len);
if (_ofs.good() == false) {
std::cerr << "日志输出到文件失败" << std::endl;
}
}
private:
std::ofstream _ofs;
};
class RollBySizeSink : public LogSink {
public:
RollBySizeSink(const std::string &basename, const std::string &suffixname, size_t max_size)
: _basename(basename), _suffixname(suffixname), _max_size(max_size) {
util::file::create_directory(util::file::path(basename));
}
virtual void log(const char *data, size_t len) override {
initLogFile();
_ofs.write(data, len);
if (_ofs.good() == false) {
std::cerr << "日志输出到文件失败" << std::endl;
}
else {
_cur_size += len;
}
}
private:
void initLogFile() {
if (_ofs.is_open() == false || _cur_size >= _max_size) {
_ofs.close();
std::string name = createFilename();
_ofs.open(name, std::ios::binary | std::ios::app);
assert(_ofs.is_open());
_cur_size = 0;
}
}
std::string createFilename() {
time_t t = util::data::now();
struct tm lt;
localtime_r(&t, <);
char tmp[32];
strftime(tmp, 32, "-%Y%m%d%H%M%S-", <);
// 如果在一秒内产生大量日志可能会导致重复打开同一个文件进行写入,这里加上一个递增的后缀
return _basename + tmp + std::to_string(_suffix++) + _suffixname;
}
private:
std::string _basename;
std::string _suffixname;
size_t _max_size;
size_t _cur_size;
size_t _suffix;
std::ofstream _ofs;
};
class RollByTimeSink : public LogSink {
public:
enum class TimeGap {
GAP_SECOND,
GAP_MINUTE,
GAP_HOUR,
GAP_DAY
};
public:
RollByTimeSink(const std::string &basename, const std::string &suffixname, TimeGap time_gap)
: _basename(basename), _suffixname(suffixname), _old_time(0) {
util::file::create_directory(util::file::path(basename));
}
virtual void log(const char *data, size_t len) override {
initLogFile();
_ofs.write(data, len);
if (_ofs.good() == false) {
std::cerr << "日志输出到文件失败" << std::endl;
}
}
private:
void initLogFile() {
time_t new_time = util::data::now();
if (_ofs.is_open() == false || new_time >= _old_time + _time_gap) {
_ofs.close();
std::string name = createFilename(new_time);
_ofs.open(name, std::ios::binary | std::ios::app);
assert(_ofs.is_open());
_old_time = new_time;
}
}
std::string createFilename(time_t t) {
struct tm lt;
localtime_r(&t, <);
char tmp[32];
strftime(tmp, 32, "-%Y%m%d%H%M%S", <);
return _basename + tmp + _suffixname;
}
private:
std::string _basename;
std::string _suffixname;
size_t _time_gap;
size_t _old_time;
std::ofstream _ofs;
};
class SinkFactory {
public:
template<typename SinkType, typename ...Args>
static LogSink::ptr createSink(Args &&...args) {
return std::make_shared<SinkType>(std::forward<Args>(args)...);
}
};
}
#endif
8.6 日志器类(Logger)设计(使用建造者模式)
日志器主要是用来对前面的所有模块进行整合,向外提供接口完成不同等级日志的输出,方便用户进行使用,我们需要使用日志系统打印log的时候,只需要创建Logger对象即可使用即可。
调用该对象的debug、info、warning、error、fatal等方法就可以输出自己想要的日志,支持解析可变参数列表和输出格式,即可以做到像使用printf函数一样。
本项目支持同步和异步两种模式,两个不同大日志器仅在落地方式上有所不同。
因此日志器类在设计时需要先设计一个Logger基类,在此基础上继承出SyncLogger同步日志器和AsyncLogger异步日志器。
日志器类管理的成员:
- 格式化模式对象
- 落地模块对象数组(一个日志器可能会向多个位置进行输出)
- 默认的日志输出限制等级(大于等于限制等级的日志才能输出)
- 互斥锁(保证日志输出的过程是线程安全的,不会出现交叉日志)
- 日志器名称(日志器的唯一标识,以便于查找)
日志器类提供的操作:
- debug等级日志的输出操作(分别封装体制消息LogMsg--各个接口日志等级的不同)
- info等级日志的输出操作
- warning等级日志的输出操作
- error等级日志的输出操作
- fatal等级日志的输出操作
具体实现:
- 抽象Logger基类(派生同步/异步日志器类)
- 两种不同的日志器,只有落地方式不同,因此将落地操作抽象出来
- 不同的日志器调用各自的落地操作进行落地
- 模块关联中使用基类指针对子类日志器对象进行日志管理和操作
cpp
// logger.hpp
/*
日志器类模块
1. 日志器基类
定义一个log纯虚函数,用于输出日志,具体输出方式由同步/异步派生类实现
实现各等级的日志输出,这些日志输出最终调用log接口完成
2. 同步/异步日志器派生类
这两个派生类分别实现同步和异步的落地过程
3. 建造者基类:
构造好各类信息,由局部/全局建造者派生类具体实现建造指定的日志器类
4. 局部/全局建造者派生类
分别实现建造局部的派生类和全局的派生类
*/
#ifndef __M_LOG_H__
#define __M_LOG_H__
#include <mutex>
#include <cstdarg>
#include "format.hpp"
#include "sink.hpp"
namespace salog {
class Logger {
public:
enum class Type {
LOGGER_SYNC,
LOGGER_ASYNC
};
using ptr = std::shared_ptr<Logger>;
Logger(const std::string &name,
Formatter::ptr formatter,
std::vector<LogSink::ptr> &sinks,
LogLevel::value level = LogLevel::value::DEBUG)
: _name(name), _limit_level(level), _formatter(formatter)
, _sinks(sinks.begin(), sinks.end()) {}
std::string name() { return _name;}
void debug(const std::string &file, size_t line, const std::string &fmt, ...) {
if (LogLevel::value::DEBUG < _limit_level) return;
va_list ap;
va_start(ap, fmt);
_log(LogLevel::value::DEBUG, file, line, fmt, ap);
va_end(ap);
}
void info(const std::string &file, size_t line, const std::string &fmt, ...) {
if (LogLevel::value::INFO < _limit_level) return;
va_list ap;
va_start(ap, fmt);
_log(LogLevel::value::INFO, file, line, fmt, ap);
va_end(ap);
}
void warning(const std::string &file, size_t line, const std::string &fmt, ...) {
if (LogLevel::value::WARNING < _limit_level) return;
va_list ap;
va_start(ap, fmt);
_log(LogLevel::value::WARNING, file, line, fmt, ap);
va_end(ap);
}
void error(const std::string &file, size_t line, const std::string &fmt, ...) {
if (LogLevel::value::ERROR < _limit_level) return;
va_list ap;
va_start(ap, fmt);
_log(LogLevel::value::ERROR, file, line, fmt, ap);
va_end(ap);
}
void fatal(const std::string &file, size_t line, const std::string &fmt, ...) {
if (LogLevel::value::FATAL < _limit_level) return;
va_list ap;
va_start(ap, fmt);
_log(LogLevel::value::FATAL, file, line, fmt, ap);
va_end(ap);
}
protected:
virtual void log(const char* data, size_t len) = 0;
private:
void _log(LogLevel::value level, const std::string &file, size_t line, const std::string &fmt, va_list ap) {
// 1. 格式化日志消息
char *buf;
int ret = vasprintf(&buf, fmt.c_str(), ap);
if (ret == -1) {
std::cerr << "格式化日志消息失败" << std::endl;
return;
}
std::string payload(buf);
free(buf);
// 2. 构造日志
LogMsg msg(file, line, _name, level, std::move(payload));
std::string str = _formatter->format(msg);
log(str.c_str(), str.size());
}
protected:
std::mutex _mutex;
std::string _name;
LogLevel::value _limit_level;
Formatter::ptr _formatter;
std::vector<LogSink::ptr> _sinks;
};
class SyncLogger : public Logger {
public:
SyncLogger(const std::string &name,
Formatter::ptr formatter,
std::vector<LogSink::ptr> &sinks,
LogLevel::value level = LogLevel::value::DEBUG)
: Logger(name, formatter, sinks, level) {
std::cout << LogLevel::toString(_limit_level) << " 等级同步日志器 " << _name << " 创建成功..." << std::endl;
}
private:
virtual void log(const char* data, size_t len) override {
for (auto &sink : _sinks) {
sink->log(data, len);
}
}
};
class AsyncLogger : public Logger {
// TODO...
};
class LoggerBuilder {
public:
using ptr = std::shared_ptr<LoggerBuilder>;
LoggerBuilder()
: _logger_type(Logger::Type::LOGGER_SYNC)
, _limit_level(LogLevel::value::DEBUG) {}
void buildLoggerName(const std::string &name) { _logger_name = name; }
void buildLoggerFormatter(const std::string &pattern) { _formatter = std::make_shared<Formatter>(pattern); }
void buildLoggerFormatter(Formatter::ptr formatter) { _formatter = formatter; }
void buildLoggerlevel(LogLevel::value level) { _limit_level = level; }
void buildLoggerType(Logger::Type type) { _logger_type = type; }
template<typename SinkType, typename ...Args>
void buildSink(Args ...args) { _sinks.push_back(SinkFactory::createSink<SinkType>(std::forward<Args>(args)...)); }
virtual Logger::ptr build() = 0;
protected:
Logger::Type _logger_type;
std::string _logger_name;
LogLevel::value _limit_level;
Formatter::ptr _formatter;
std::vector<LogSink::ptr> _sinks;
};
class LocalLoggerBuilder : public LoggerBuilder {
public:
virtual Logger::ptr build() {
if (_logger_name.empty()) {
std::cerr << "日志器不能为空" << std::endl;
exit(1);
}
if (_formatter.get() == nullptr) {
std::cout << "当前日志器 " << _logger_name << " 未检测到日志格式,将使用默认格式..." << std::endl;
_formatter = std::make_shared<Formatter>();
}
if (_sinks.empty()) {
std::cout << "当前日志器 " << _logger_name << " 未检测到日志落地方向,将使用标准输出..." << std::endl;
_sinks.push_back(SinkFactory::createSink<StdoutSink>());
}
Logger::ptr logger;
if (_logger_type == Logger::Type::LOGGER_SYNC) {
logger = std::make_shared<SyncLogger>(_logger_name, _formatter, _sinks, _limit_level);
}
else {
// 异步日志器 TODO...
}
return logger;
}
};
}
#endif
8.7 双缓冲区异步任务处理器(AsyncLooper)设计
设计思想:异步处理线程+线程池
日用这将需要完成的任务添加到线程池中,由异步线程来完成任务的实际执行操作。
任务池的设计思想:双缓冲区阻塞数据池。
优势:避免了空间的频繁申请释放,且京可能的减少了生产者与消费者之间锁冲突的概率,提高了任务处理效率。
这里的双缓冲区是将一个缓冲区中的任务全部处理完后,交换两个缓冲区,重写对新的缓冲区中的任务进行处理,虽然多线程写入时也会冲突,但是冲突并不会像每次只处理一条信息的情况频繁(减少了生产者和消费者之间锁的冲突),且不涉及到空间的频繁申请释放所带来的消耗。

8.7.1 缓冲区类的设计与实现
缓冲区类的设计:
- 管理一个存放字符串数据的缓冲区(使用string进行空间管理)
- 为了防止频繁的空间申请,先对string初始化出固定的大小,在后面的操作中不再进行push和pop,而是在已经申请出来的空间中进行读取和写入。
- 通过两个指针进行管理,一个指向写入数据的位置,一个指向读取数据的位置,两个指针相遇则说明当前无内容。
提供的操作:
- 判空
- 可读大小
- 可写大小
- 重置读写指针
- 交换两个缓冲区的内容
- 写入
- 返回可读内容的头指针
cpp
// buffer.hpp
/*
缓冲区类模块
实现一个缓冲区类,作为异步输出的双缓冲区基础
成员变量:
1. 读指针,从这个位置开始读
2. 写指针,从这个位置开始写
3. 一个字符串,作为数据的实际缓冲区
成员函数:
1. 判空
2. 可读大小
3. 可写大小
4. 重置读写指针
5. 交换两个缓冲区的内容
6. 写入
7. 返回可读内容的头指针
*/
#ifndef __M_BUF_H__
#define __M_BUF_H__
#include <string>
#define BUFFER_DEFAULT_SIZE (1 * 1024 * 1024) // 缓冲区默认大小
#define BUFFER_INCREMENT_SIZE (1 * 1024 * 1024) // 线性增长大小
#define BUFFER_THRESHOLD_SIZE (8 * 1024 * 1024) // 指数增长阈值,如果超过这个阈值,线性增长
namespace salog {
class Buffer {
public:
Buffer(): _reader_idx(0), _writer_idx(0), _buf(BUFFER_DEFAULT_SIZE, '\0') {}
bool empty() { return _reader_idx == _writer_idx; }
size_t readAbleSize() { return _writer_idx - _reader_idx; }
size_t writeAbleSize() { return _buf.size() - _writer_idx; }
void reset() { _reader_idx = _writer_idx = 0; }
void swap(Buffer &buf) {
if (this == &buf) return;
std::swap(_reader_idx, buf._reader_idx);
std::swap(_writer_idx, buf._writer_idx);
_buf.swap(buf._buf);
}
void push(const char *data, size_t len) {
ensureEnoughSpace(len);
std::copy(data, data + len, &_buf[_writer_idx]);
_writer_idx += len;
}
const char *begin() { return &_buf[_reader_idx]; }
private:
// 剩余空间不过则扩容
void ensureEnoughSpace(size_t len) {
if (len <= writeAbleSize()) return;
size_t new_capacity = 0;
// 未达阈值,指数增长,达到阈值,线性增长,为防止扩容完一次还不够,再加上len
if (_buf.size() < BUFFER_THRESHOLD_SIZE) new_capacity = _buf.size() * 2 + len;
else new_capacity = _buf.size() + BUFFER_INCREMENT_SIZE + len;
_buf.resize(new_capacity);
}
private:
size_t _reader_idx;
size_t _writer_idx;
std::string _buf;
};
}
#undef BUFFER_DEFAULT_SIZE
#undef BUFFER_INCREMENT_SIZE
#undef BUFFER_THRESHOLD_SIZE
#endif
8.7.2 异步工作器的设计与实现
异步工作器:
- 一部工作其使用双缓冲区思想
- 外界将任务数据添加到输入缓冲区中
- 异步线程对处理缓冲区中的数据进行处理,若处理缓冲区中没有数据了就交换缓冲区
管理的成员:
- 双缓冲区(生产,消费)
- 互斥锁,保证线程安全
- 条件变量,生产&消费(生产缓冲区中没有数据,处理完消费缓冲区数据后就休眠)
- 回调函数(针对缓冲区中数据的处理接口,外界传入一个函数,告诉异步工作器数据该如何处理)
提供的操作:
- 停止异步工作器
- 添加数据到缓冲区
私有操作:
- 创建线程,线程日后函数中,交换缓冲区,对消费缓冲区使用回调函数进行处理,处理完再交换。
cpp
// looper.hpp
/*
异步日志工作器模块(生产者消费者模式):封装双缓冲区和异步线程的工作逻辑,并提供push接口,方便其他线程进行输入
成员变量:
1. 互斥锁,对生产者线程和消费者线程的公共访问资源(缓冲区的交换)进行加锁。
2. 线程运行标记位,atomic的bool类型,保证对其操作是原子的。
3. 条件变量,两个条件变量,分别用于限制生产者线程和消费者线程,相互唤醒。
4. 两个缓冲区,分别是写入缓冲区和输出缓冲区,对两个缓冲区的交换行为进行加锁。
5. 回调函数,由外部传入,用于补全消费者线程的具体落地行为。
6. 消费者线程,工作器创建时进行初始化,不断地从缓冲区中获取信息,落地到具体方向。
成员函数:
1. stop:修改标记位,将缓冲区标记为非运行状态,并唤醒消费者线程,完成剩余工作后退出。
2. push:提供给外界的生产者进行写入日志。
3. worker_loop:私有成员,消费者线程的主逻辑,具体落地的回调函数由外界传入。
*/
#ifndef __M_LOOPER_H__
#define __M_LOOPER_H__
#include <thread>
#include <condition_variable>
#include <mutex>
#include <atomic>
#include <memory>
#include <functional>
#include "buffer.hpp"
namespace salog {
class AsyncLooper {
public:
using Functor = std::function<void(Buffer &buffer)>;
using ptr = std::shared_ptr<AsyncLooper>;
AsyncLooper(const Functor &cb)
: _isrunning(true)
, _looper_callback(cb)
, _thread(std::thread(&AsyncLooper::worker_loop, this)) {}
~AsyncLooper() { if (_isrunning) stop(); }
void stop() {
_isrunning = false;
_con_cond.notify_all();
_thread.join();
}
void push(const char *data, size_t len) {
// 只有运行状态才允许写入
if (!_isrunning) return;
// 加锁的作用域
{
std::unique_lock<std::mutex> lock(_mutex);
// 输入缓冲区空间大小足够才允许写入
_pro_cond.wait(lock, [&](){ return _pro_buf.writeAbleSize() >= len; });
_pro_buf.push(data, len);
}
// 完成写入,唤醒消费者
_con_cond.notify_all();
}
private:
void worker_loop() {
while (true) {
{
std::unique_lock<std::mutex> lock(_mutex);
// 如果为非运行状态且生产缓冲区已经没有消息,退出
if (!_isrunning && _pro_buf.empty()) return;
_con_cond.wait(lock, [&](){ return !_isrunning || !_pro_buf.empty(); });
_pro_buf.swap(_con_buf);
}
_pro_cond.notify_all();
_looper_callback(_con_buf);
_con_buf.reset();
}
}
private:
std::mutex _mutex;
std::atomic<bool> _isrunning;
std::condition_variable _pro_cond; // 生产者条件变量
std::condition_variable _con_cond; // 消费者条件变量
Buffer _pro_buf; // 生产缓冲区
Buffer _con_buf; // 消费缓冲区
Functor _looper_callback;
std::thread _thread;
};
}
#endif
8.8 异步日志器(AsyncLogger)的设计
异步日志器继承自日志器类,并在同步日志器上拓展了异步消息处理器,当我们需要异步输出日志的时候,需要创建异步日志器和消息处理器,调用异步日志器的log、debug、info等函数输出不同级别的日志。
- log函数重写Logger类的log函数,主要实现将日志数据加入到异步队列缓冲区中
- realLog函数主要由异步线程进行调用(是为异步消息处理设置的回调函数)完成日志的实际落地工作。
完成后还需要完善日志器建造者,进行异步日志器安全模式的选择,提供异步日志器的创建。
cpp
// logger.hpp
class AsyncLogger : public Logger {
public:
AsyncLogger(const std::string &name,
Formatter::ptr formatter,
std::vector<LogSink::ptr> &sinks,
LogLevel::value level = LogLevel::value::DEBUG,
AsyncLooper::AsyncType looper_type = AsyncLooper::AsyncType::ASYNC_SAFE)
: Logger(name, formatter, sinks, level)
, _looper_type(looper_type)
, _looper(std::make_shared<AsyncLooper>(std::bind(&AsyncLogger::call_back_log, this, std::placeholders::_1), _looper_type)) {
std::cout << LogLevel::toString(_limit_level) << " 等级异步日志器 " << _name << " 创建成功..." << std::endl;
}
private:
virtual void log(const char* data, size_t len) override {
_looper->push(data, len);
}
void call_back_log(Buffer &buf) {
for (auto &sink : _sinks) {
sink->log(buf.begin(), buf.readAbleSize());
}
}
private:
AsyncLooper::AsyncType _looper_type;
AsyncLooper::ptr _looper;
};
8.9 单例日志器管理类设计(单例模式)
日志的输出,我们希望能够在项目的任意位置进行,如果由我们自己创建日志器,就会受到日志器所在作用域访问属性限制。
因此,为了突破访问域的限制,我们创建一个日志器管理类,这是一个单例类,我们通过管理器单例在项目的任意位置获取到指定的日志器,来进行日志输出。
日志器管理器:
-
作用:
- 对所有创建的日志器进行管理
- 可与i在程序的任意位置获取相同的单例对象,获取其中的日志器进行日志输出
-
特性:将管理器设计为单例
-
拓展:创建时,默认创建一个日志器(用于进行标准输出的打印),方便用户使用
-
管理的成员:
- 默认日志器
- 所管理的日志器数组
- 互斥锁,对数组的访问进行加锁,防止多个线程同时访问数组
-
提供的接口:
- 添加日志器
- 判断是否管理了指定名称的日志器
- 获取指定名称的日志器
- 获取默认日志器
cpp
// logger.hpp
class loggerManager {
private:
loggerManager() {
// 默认构造一个名为root的同步日志器
std::unique_ptr<LocalLoggerBuilder> slb(new LocalLoggerBuilder());
slb->buildLoggerName("root");
_root_logger = slb->build();
_loggers[_root_logger->name()] = _root_logger;
}
loggerManager(const loggerManager&) = delete;
loggerManager operator=(const loggerManager&) = delete;
public:
static loggerManager& getInstance() {
static loggerManager lm; // C++11开始,这里的static变量的操作为原子的
return lm;
}
bool hasLogger(const std::string &name) {
std::unique_lock<std::mutex> lock(_mutex);
return _loggers.count(name) > 0;
}
void addLogger(const std::string &name, const Logger::ptr &logger) {
if (hasLogger(name)) return;
std::unique_lock<std::mutex> lock(_mutex);
_loggers[name] = logger;
}
Logger::ptr getLogger(const std::string &name) {
if (!hasLogger(name)) return Logger::ptr();
std::unique_lock<std::mutex> lock(_mutex);
return _loggers[name];
}
Logger::ptr rootLogger() { return _root_logger; }
private:
std::mutex _mutex;
Logger::ptr _root_logger;
std::unordered_map<std::string, Logger::ptr> _loggers;
};
class GlobalLoggerBuilder : public LoggerBuilder {
public:
virtual Logger::ptr build() {
if (_logger_name.empty()) {
std::cerr << "日志器不能为空" << std::endl;
exit(1);
}
if (loggerManager::getInstance().hasLogger(_logger_name)) {
std::cout << "当前日志器 " << _logger_name << " 已经存在,不进行建造,直接返回...";
return loggerManager::getInstance().getLogger(_logger_name);
}
if (_formatter.get() == nullptr) {
std::cout << "当前日志器 " << _logger_name << " 未检测到日志格式,将使用默认格式..." << std::endl;
_formatter = std::make_shared<Formatter>();
}
if (_sinks.empty()) {
std::cout << "当前日志器 " << _logger_name << " 未检测到日志落地方向,将使用标准输出..." << std::endl;
_sinks.push_back(SinkFactory::createSink<StdoutSink>());
}
Logger::ptr logger;
if (_logger_type == Logger::Type::LOGGER_SYNC) {
logger = std::make_shared<SyncLogger>(_logger_name, _formatter, _sinks, _limit_level);
}
else {
logger = std::make_shared<AsyncLogger>(_logger_name, _formatter, _sinks, _limit_level, _looper_type);
}
loggerManager::getInstance().addLogger(logger->name(), logger);
return logger;
}
};
8.10 日志宏&全局接口设计(代理模式)
提供获取日志器的全局接口,方便用户使用。
使用代理模式通过宏函数来代理Logger类的log、debug等接口,以便控制源码文件名称和行号的输出控制,简化用户操作
cpp
// mylog.h
/*
日志器头文件
提供使用日志器的全局接口,用户仅需包含这个头文件即可使用日志系统
使用代理模式通过宏函数来代理debug等接口,让用户不必传递文件名、行号等参数
方便用户进行使用
*/
#ifndef __M_SALOG_H__
#define __M_SALOG_H__
#include "logger.hpp"
namespace salog {
// 获取局部日志器建造器
LocalLoggerBuilder getLocalLoggerBuilder() { return LocalLoggerBuilder(); }
// 获取全局日志器建造器
GlobalLoggerBuilder getGlobalLoggerBuilder() { return GlobalLoggerBuilder(); }
// 获取默认日志器
Logger::ptr rootLogger() { return loggerManager::getInstance().rootLogger(); }
// 获取指定日志器
Logger::ptr getLogger(const std::string &name) { return loggerManager::getInstance().getLogger(name); }
#define debug(fmt, ...) debug(__FILE__, __LINE__, fmt, ##__VA_ARGS__)
#define info(fmt, ...) info(__FILE__, __LINE__, fmt, ##__VA_ARGS__)
#define warning(fmt, ...) warning(__FILE__, __LINE__, fmt, ##__VA_ARGS__)
#define error(fmt, ...) error(__FILE__, __LINE__, fmt, ##__VA_ARGS__)
#define fatal(fmt, ...) fatal(__FILE__, __LINE__, fmt, ##__VA_ARGS__)
// 使用指定日志器打印指定等级日志
#define LOG_DEBUG(logger, fmt, ...) (logger)->debug(fmt, ##__VA_ARGS__)
#define LOG_INFO(logger, fmt, ...) (logger)->info(fmt, ##__VA_ARGS__)
#define LOG_WARNING(logger, fmt, ...) (logger)->warning(fmt, ##__VA_ARGS__)
#define LOG_ERROR(logger, fmt, ...) (logger)->error(fmt, ##__VA_ARGS__)
#define LOG_FATAL(logger, fmt, ...) (logger)->fatal(fmt, ##__VA_ARGS__)
// 使用默认日志器打印指定等级日志
#define DEBUG(fmt, ...) LOG_DEBUG(salog::rootLogger(), fmt, ##__VA_ARGS__)
#define INFO(fmt, ...) LOG_INFO(salog::rootLogger(), fmt, ##__VA_ARGS__)
#define WARNING(fmt, ...) LOG_WARNING(salog::rootLogger(), fmt, ##__VA_ARGS__)
#define ERROR(fmt, ...) LOG_ERROR(salog::rootLogger(), fmt, ##__VA_ARGS__)
#define FATAL(fmt, ...) LOG_FATAL(salog::rootLogger(), fmt, ##__VA_ARGS__)
}
#endif
9. 功能用例
cpp
// testcase.cc
#include "../log/salog.h"
void loggerTest(const std::string &logger_name)
{
salog::Logger::ptr lp = salog::getLogger(logger_name);
assert(lp.get());
FATAL("------------example--------------------");
lp->debug("%s", "logger->debug");
lp->info("%s", "logger->info");
lp->warning("%s", "logger->warn");
lp->error("%s", "logger->error");
lp->fatal("%s", "logger->fatal");
LOG_DEBUG(lp, "%s", "LOG_DEBUG");
LOG_INFO(lp, "%s", "LOG_INFO");
LOG_WARNING(lp, "%s", "LOG_WARN");
LOG_ERROR(lp, "%s", "LOG_ERROR");
LOG_FATAL(lp, "%s", "LOG_FATAL");
FATAL("---------------------------------------");
std::string log_msg = "hello world-";
size_t fsize = 0;
size_t count = 0;
while (count < 1000000)
{
std::string msg = log_msg + std::to_string(count++);
lp->error("%s", msg.c_str());
}
}
int main(int argc, char *argv[])
{
// 获取全局日志器建造者
auto lbp = salog::getGlobalLoggerBuilder();
lbp.buildLoggerName("stdout_and_file_logger"); // 设置日志器名称
lbp.buildLoggerFormatter("[%d][%c][%f:%l][%p] %m%n"); // 设置日志输出格式
lbp.buildLoggerLevel(salog::LogLevel::value::DEBUG); // 设置日志限制输出等级
lbp.buildSink<salog::StdoutSink>(); // 创建一个标准输出的落地方向
lbp.buildSink<salog::FileSink>("./logs/sync.log"); // 创建一个文件落地方向
lbp.buildSink<salog::RollBySizeSink>("./logs/size", ".log", 30 * 1024 * 1024); // 创建滚动日志落地方向
lbp.buildSink<salog::RollByTimeSink>("./logs/time", ".log", salog::RollByTimeSink::TimeGap::GAP_MINUTE); // 创建滚动日志落地方向
lbp.buildLoggerType(salog::Logger::Type::LOGGER_SYNC); // 设置日志器类型为同步日志
lbp.build(); // 建造日志器
loggerTest("stdout_and_file_logger");
return 0;
}
10. 性能测试
下面对日志系统做一个性能测试。
测试方法:每秒输出日志数=输出日志条数/总时间
主要测试要素:同步/异步 & 单线程/多线程 & 不同落地方向
- 100w+条指定长度的日志输出所耗时间
- 每秒输出条数
测试环境:
-
云服务器:4核4G
-
CPU具体参数如下
czxyv@czxyv:~/Data/sync-async_log/example$ lscpu Architecture: x86_64 CPU op-mode(s): 32-bit, 64-bit Address sizes: 46 bits physical, 48 bits virtual Byte Order: Little Endian CPU(s): 4 On-line CPU(s) list: 0-3 Vendor ID: GenuineIntel Model name: Intel(R) Xeon(R) Platinum 8255C CPU @ 2.50GHz CPU family: 6 Model: 85 Thread(s) per core: 1 Core(s) per socket: 4 Socket(s): 1 Stepping: 5 BogoMIPS: 4988.28 Flags: fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush mmx fxsr sse sse2 ss ht syscall nx pdpe1gb rdtscp lm constant_tsc rep_good nopl cpuid tsc_known_freq pni pclmulqdq ssse3 fma cx16 pcid sse4_1 sse4_2 x2apic movbe popc nt tsc_deadline_timer aes xsave avx f16c rdrand hypervisor lahf_lm abm 3dnowprefetch pti fsgsbase bmi1 hle avx2 smep bmi2 e rms invpcid rtm mpx avx512f avx512dq rdseed adx smap clflushopt clwb avx512cd avx512bw avx512vl xsaveopt xsavec xgetbv1 ara t avx512_vnni Virtualization features: Hypervisor vendor: KVM Virtualization type: full Caches (sum of all): L1d: 128 KiB (4 instances) L1i: 128 KiB (4 instances) L2: 16 MiB (4 instances) L3: 35.8 MiB (1 instance) NUMA: NUMA node(s): 1 NUMA node0 CPU(s): 0-3 Vulnerabilities: Gather data sampling: Unknown: Dependent on hypervisor status Itlb multihit: KVM: Mitigation: VMX unsupported L1tf: Mitigation; PTE Inversion Mds: Vulnerable: Clear CPU buffers attempted, no microcode; SMT Host state unknown Meltdown: Mitigation; PTI Mmio stale data: Vulnerable: Clear CPU buffers attempted, no microcode; SMT Host state unknown Reg file data sampling: Not affected Retbleed: Vulnerable Spec rstack overflow: Not affected Spec store bypass: Vulnerable Spectre v1: Mitigation; usercopy/swapgs barriers and __user pointer sanitization Spectre v2: Mitigation; Retpolines; STIBP disabled; RSB filling; PBRSB-eIBRS Not affected; BHI Retpoline Srbds: Not affected Tsx async abort: Vulnerable: Clear CPU buffers attempted, no microcode; SMT Host state unknown -
内存具体参数如下:
czxyv@czxyv:~/Data/sync-async_log/example$ sudo dmidecode -t memory [sudo] password for czxyv: # dmidecode 3.5 Getting SMBIOS data from sysfs. SMBIOS 2.8 present. Handle 0x1000, DMI type 16, 23 bytes Physical Memory Array Location: Other Use: System Memory Error Correction Type: Multi-bit ECC Maximum Capacity: 4 GB Error Information Handle: Not Provided Number Of Devices: 1 Handle 0x1100, DMI type 17, 40 bytes Memory Device Array Handle: 0x1000 Error Information Handle: Not Provided Total Width: Unknown Data Width: Unknown Size: 4 GB Form Factor: DIMM Set: None Locator: DIMM 0 Bank Locator: Not Specified Type: RAM Type Detail: Other Speed: Unknown Manufacturer: Smdbmds Serial Number: Not Specified Asset Tag: Not Specified Part Number: Not Specified Rank: Unknown Configured Memory Speed: Unknown Minimum Voltage: Unknown Maximum Voltage: Unknown Configured Voltage: Unknown
-
-
操作系统:Ubuntu Server 24.04 LTS 64bit
测试代码
cpp
// bench.h
#ifndef __M_BENCH_H__
#define __M_BENCH_H__
#include "../log/salog.h"
#include <chrono>
namespace salog {
void bench(const std::string &loger_name, size_t thread_num, size_t msglen, size_t msg_count) {
auto lp = salog::getLogger(loger_name);
if (lp.get() == nullptr) return;
std::string msg(msglen, '1');
size_t msg_count_per_thread = msg_count / thread_num;
std::vector<double> cost_time(thread_num);
std::vector<std::thread> threads;
std::cout << "输入线程数量: " << thread_num << std::endl;
std::cout << "输出日志数量: " << msg_count << std::endl;
std::cout << "输出日志大小: " << msglen * msg_count / 1024 << "KB" << std::endl;
for (int i = 0; i < thread_num; i++) {
threads.emplace_back([&, i]() {
auto start = std::chrono::high_resolution_clock::now();
for(size_t j = 0; j < msg_count_per_thread; j++) lp->fatal("%s", msg.c_str());
auto end = std::chrono::high_resolution_clock::now();
auto cost = std::chrono::duration_cast<std::chrono::duration<double>>(end - start);
cost_time[i] = cost.count();
auto avg = msg_count_per_thread / cost_time[i];
std::cout << "线程" << i << "耗时: " << cost.count() << "s";
std::cout << " 平均:" << (size_t)avg << "/s\n"; });
}
for (auto &thr : threads) thr.join();
double max_cost = 0;
for (auto cost : cost_time) max_cost = max_cost < cost ? cost : max_cost;
std::cout << "总消耗时间: " << max_cost << std::endl;
std::cout << "平均每秒输出: " << (size_t)(msg_count / max_cost) << std::endl;
}
}
#endif
cpp
#include "../log/salog.h"
#include "bench.h"
#include <unistd.h>
void sync_bench_thread_log(size_t thread_count, size_t msg_count, size_t msglen) {
static int num = 1;
std::string logger_name = "sync_bench_logger" + std::to_string(num++);
INFO("************************************************");
INFO("同步日志测试: %d threads, %d messages", thread_count, msg_count);
auto lbp = salog::getGlobalLoggerBuilder();
lbp.buildLoggerName(logger_name);
lbp.buildLoggerFormatter("%m%n");
lbp.buildSink<salog::FileSink>("./logs/sync.log");
lbp.buildLoggerType(salog::Logger::Type::LOGGER_SYNC);
lbp.build();
salog::bench(logger_name, thread_count, msglen, msg_count);
INFO("************************************************");
}
void async_bench_thread_log(size_t thread_count, size_t msg_count, size_t msglen) {
static int num = 1;
std::string logger_name = "async_bench_logger" + std::to_string(num++);
INFO("************************************************");
INFO("异步日志测试: %d threads, %d messages", thread_count, msg_count);
auto lbp = salog::getGlobalLoggerBuilder();
lbp.buildLoggerName(logger_name);
lbp.buildLoggerFormatter("%m");
lbp.buildSink<salog::FileSink>("./logs/async.log");
lbp.buildLoggerType(salog::Logger::Type::LOGGER_ASYNC);
lbp.build();
salog::bench(logger_name, thread_count, msglen, msg_count);
INFO("************************************************");
}
void bench_test() {
// 同步写日志
sync_bench_thread_log(1, 1000000, 100);
sync_bench_thread_log(5, 1000000, 100);
// 异步日志输出,为了避免因为等待落地影响时间所以日志数量降低为小于缓冲区大小进行测试
async_bench_thread_log(1, 100000, 100);
async_bench_thread_log(5, 100000, 100);
}
int main()
{
bench_test();
return 0;
}
测试结果:
当前日志器 root 未检测到日志格式,将使用默认格式...
当前日志器 root 未检测到日志落地方向,将使用标准输出...
DEBUG 等级同步日志器 root 创建成功...
[2026:03:18 17:48:46][139367045220160][test.cc:8][root][INFO] ************************************************
[2026:03:18 17:48:46][139367045220160][test.cc:9][root][INFO] 同步日志测试: 1 threads, 1000000 messages
DEBUG 等级同步日志器 sync_bench_logger1 创建成功...
输入线程数量: 1
输出日志数量: 1000000
输出日志大小: 97656KB
线程0耗时: 1.04592s 平均:956094/s
总消耗时间: 1.04592
平均每秒输出: 956094
[2026:03:18 17:48:47][139367045220160][test.cc:17][root][INFO] ************************************************
[2026:03:18 17:48:47][139367045220160][test.cc:8][root][INFO] ************************************************
[2026:03:18 17:48:47][139367045220160][test.cc:9][root][INFO] 同步日志测试: 5 threads, 1000000 messages
DEBUG 等级同步日志器 sync_bench_logger2 创建成功...
输入线程数量: 5
输出日志数量: 1000000
输出日志大小: 97656KB
线程0耗时: 0.599416s 平均:333657/s
线程1耗时: 0.603972s 平均:331141/s
线程3耗时: 0.603602s 平均:331344/s
线程4耗时: 0.616288s 平均:324523/s
线程2耗时: 0.637972s 平均:313493/s
总消耗时间: 0.637972
平均每秒输出: 1567465
[2026:03:18 17:48:48][139367045220160][test.cc:17][root][INFO] ************************************************
[2026:03:18 17:48:48][139367045220160][test.cc:23][root][INFO] ************************************************
[2026:03:18 17:48:48][139367045220160][test.cc:24][root][INFO] 异步日志测试: 1 threads, 100000 messages
DEBUG 等级异步日志器 async_bench_logger1 创建成功...
输入线程数量: 1
输出日志数量: 100000
输出日志大小: 9765KB
线程0耗时: 0.151698s 平均:659205/s
总消耗时间: 0.151698
平均每秒输出: 659205
[2026:03:18 17:48:48][139367045220160][test.cc:32][root][INFO] ************************************************
[2026:03:18 17:48:48][139367045220160][test.cc:23][root][INFO] ************************************************
[2026:03:18 17:48:48][139367045220160][test.cc:24][root][INFO] 异步日志测试: 5 threads, 100000 messages
DEBUG 等级异步日志器 async_bench_logger2 创建成功...
输入线程数量: 5
输出日志数量: 100000
输出日志大小: 9765KB
线程0耗时: 0.0705695s 平均:283408/s
线程4耗时: 0.0721728s 平均:277112/s
线程3耗时: 0.0789279s 平均:253395/s
线程2耗时: 0.0803689s 平均:248852/s
线程1耗时: 0.0811993s 平均:246307/s
总消耗时间: 0.0811993
平均每秒输出: 1231537
[2026:03:18 17:48:48][139367045220160][test.cc:32][root][INFO] ************************************************
总结:
-
同步日志器单线程输出:956094条/s
-
同步日志器多线程输出:1567465条/s
-
异步日志器单线程输出:659205条/s
-
异步日志器多线程输出:1231537条/s
通过上面的数据不难看出,异步输出的效率还没有同步高,这是因为,异步输出时,由于异步日志中存在较多的锁冲突,因此性能也会有一定的降低,但是同步日志是单线程的操作,IO操作也是由这个给工作线程来完成,在实际生产中,工作线程并不是单单打日志,此时将IO操作交给专门的线程,可以提高工作线程的效率。
另外的我们可以发现同步多线程中的每个线程的效率相对于单线程要低不少,这是由于限制同步日志效率的最大原因是磁盘性能,打日志的线程多少并明显区别,线程多了以后返回会降低每个线程的效率,这是因为增加了线程对磁盘的读写争抢。
而对于异步多线程来说,效率取决于cpu性能,因为异步多线程同时写入只是写入到内存,并不涉及IO操作,所以异步日志多线程输出的提升要相对于同步的提升更大。
11. 扩展
对于落地方向,支持扩展,只需在sink.hpp中通过继承LogSink类,实现新的产品类,就可以支持。
扩展思路:
- 通过网络传输落地到日志服务器
- 落地日志到数据库
12. 完整代码
由于篇幅太长,就不在这里展示了,可自行查阅:Sync-Async_log: 一个简单的,基于多设计模式的同步异步日志器