可变参数模板+异步队列实现异步打印功能

场景

公司有成熟的日志库,但是打印日志的功能是在调用线程里触发的,简而言之就是哪个线程调用了日志库的打印功能,就在哪个线程里输出,如果打印功能频繁,可能会影响该线程的逻辑处理。目前公司的软件界面显示和计算等逻辑很频繁,考虑用异步的方式实现打印。

思路

1 需要把要写的参数封装成一个数据结构然后投递到异步队列里。
2 因为参数类型多变,需要用模板实现写日志的函数。参数个数也不确定,就需要用可变参数模板。
3 需要另一个线程从队列中取数据并打印日志,也可调用第三饭日志库打印。为减少线程调度开销,在队列为空时,需要将队列挂起。

具体实现

日志等级定义为几个级别

1
2
3
4
5
6
   enum LogLv {
DEBUGS = 0,
INFO = 1,
WARN = 2,
ERRORS = 3,
};

定义要投递给队列的任务结构

1
2
3
4
5
6
7
8
9
   class  LogTask {
public:
LogTask(){}
LogTask(const LogTask& src):_level(src._level), _logdatas(src._logdatas){}
LogTask(const LogTask&& src):_level(src._level),
_logdatas(std::move(src._logdatas)){}
LogLv _level;
QQueue<QVariant> _logdatas;
};

定义异步处理的日志类,先给出全部代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
  class  AsyncLog : public QThread {

public:


static AsyncLog& Instance(){
static AsyncLog instance;
return instance;
}

//如果不支持C++17,可采用如下函数入队
template<typename Arg, typename ...Args>
void TaskEnque(std::shared_ptr<LogTask> task, Arg&& arg, Args&&... args){
task->_logdatas.enqueue(QVariant(std::forward<Arg>(arg)));
qDebug() << "enque task data is " << arg << endl;
TaskEnque(task,std::forward<Args>(args)...);
}

template<typename Arg>
void TaskEnque(std::shared_ptr<LogTask> task, Arg&& arg){
task->_logdatas.enqueue(QVariant(std::forward<Arg>(arg)));
qDebug() << "enque task data is " << arg << endl;
}

//可变参数列表,异步写
template<typename... Args>
void AsyncWrite(LogLv level, Args&&... args) {
auto task = std::make_shared<LogTask>();
//折叠表达式依次将可变参数写入队列,需C++17版本支持
(task->_logdatas.enqueue(QVariant(std::forward<Args>(args))), ...);
//如不支持C++17 请用这个版本入队
//TaskEnque(task, args...);
task->_level = level;
std::unique_lock<std::mutex> lock(_mtx);
_queue.enqueue(task);
bool notify = (_queue.size() == 1)?true:false;
lock.unlock();
// 通知等待的线程有新的任务可处理
if(notify){
_empty_cond.notify_one();
}

}

void Stop(){
_b_stop = true;
_empty_cond.notify_one();
}



private:

AsyncLog(const AsyncLog&) = delete;
AsyncLog& operator = (const AsyncLog&) = delete;

AsyncLog(QObject* parent = nullptr):QThread(parent),_b_stop(false){

}

void run(){
for(;;){
std::unique_lock<std::mutex> lock(_mtx);

while(_queue.empty() && !_b_stop){
_empty_cond.wait(lock);
}

if(_b_stop){
return;
}

auto logtask = _queue.dequeue();
lock.unlock();
processTask(logtask);
}
}

void processTask(std::shared_ptr<LogTask> task){
qDebug() << "log level is " << task->_level << endl;

if(task->_logdatas.empty()){
return;
}
// 队列首元素
auto head = task->_logdatas.dequeue();
auto formatstr = head.toString().toStdString();

for(;!(task->_logdatas.empty());){
auto data = task->_logdatas.dequeue();
qDebug() << "deque task data is " << data;
formatstr=formatString(formatstr, data);

}

qDebug() << "formatstr is " << QString::fromStdString(formatstr) << endl;
}


template<typename... Args>
std::string formatString(const std::string& format, Args&&... args) {
std::string result = format;
size_t pos = 0;
//lambda表达式查找并替换字符串
auto replacePlaceholder = [&](const std::string& placeholder, const std::string& replacement) {
size_t placeholderPos = result.find(placeholder, pos);
if (placeholderPos != std::string::npos) {
result.replace(placeholderPos, placeholder.length(), replacement);
pos = placeholderPos + replacement.length();
}else{
result = result + " " + replacement;
}
};

(replacePlaceholder("{}", QVariant(std::forward<Args>(args)).toString().toStdString()), ...);

qDebug() << "result is : " << QString::fromStdString(result) << endl;
return result;
}

std::condition_variable _empty_cond;
QQueue<std::shared_ptr<LogTask> > _queue;
bool _b_stop;
std::mutex _mtx;
};

下面分别解释每段含义
我们先定义了一个单例类

1
2
3
4
static AsyncLog& Instance(){
static AsyncLog instance;
return instance;
}

通过可变参数列表,实现异步写功能

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
    //可变参数列表,异步写
template<typename... Args>
void AsyncWrite(LogLv level, Args&&... args) {
auto task = std::make_shared<LogTask>();
//折叠表达式依次将可变参数写入队列,需C++17版本支持
(task->_logdatas.enqueue(QVariant(std::forward<Args>(args))), ...);
//如不支持C++17 请用这个版本入队
//TaskEnque(task, args...);
task->_level = level;
std::unique_lock<std::mutex> lock(_mtx);
_queue.enqueue(task);
bool notify = (_queue.size() == 1)?true:false;
lock.unlock();
// 通知等待的线程有新的任务可处理
if(notify){
_empty_cond.notify_one();
}
}

AsyncWrite用到了可变参数模板,类型为模板类型的右值引用,这被称作万能引用,使用该引用有几个好处
1 支持右值传递
2 支持完美转发
函数内用到了折叠表达式,折叠表达式基本语法如下

1
(表达式, ...)

折叠表达式会循环执行表达式,直到参数列表用完。
AsyncWrite主要实现了将可变参数封装为task然后投递到队列里,如果队列由空到满则需激活消费者线程取消费队列。
如果你的编译器不支持C++17, 可以改为实现两个模板函数TaskEnque入队。

接下来的run函数重载了QThread的run函数,意在启动一个线程处理任务。

processTask 是处理打印日志的功能。
formatString 为多参数模板函数,意在模拟将多个参数匹配和拼接的功能,当然这个函数可替代为你使用的第三方库。

测试和使用

我们在主函数内初始化日志类,然后启动一个界面阻塞主线程,点击关闭界面后,会销毁日志类开辟的线程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
int main(int argc, char *argv[])
{
QApplication a(argc, argv);

auto & instance = AsyncLog::AsyncLog::Instance();
instance.AsyncWrite(AsyncLog::LogLv::ERRORS, "hello", "world !",
"my name is {} ", "zack {}", "fair");
instance.start();

MainWindow main;
main.show();

a.exec();
qDebug() << "Application Quitting, Please Wait...";
AsyncLog::AsyncLog::Instance().Stop();
// 暂停执行500毫秒(0.5秒)
std::this_thread::sleep_for(std::chrono::milliseconds(500));
qDebug() << "Application exited...";
return 0;

}

函数运行后输出如下

1
2
3
4
formatstr is  "hello world ! my name is zack fair " 

Application Quitting, Please Wait...
Application exited...

总结

本文介绍了如何用模板和可变参数构造一个异步打印功能的类,是基于QT实现的,之后又实现了C++标准版本的。源码链接如下

QT版本

C++版本