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

场景

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

思路

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

具体实现

日志等级定义为几个级别

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

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

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

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

  1. class AsyncLog : public QThread {
  2. public:
  3. static AsyncLog& Instance(){
  4. static AsyncLog instance;
  5. return instance;
  6. }
  7. //如果不支持C++17,可采用如下函数入队
  8. template<typename Arg, typename ...Args>
  9. void TaskEnque(std::shared_ptr<LogTask> task, Arg&& arg, Args&&... args){
  10. task->_logdatas.enqueue(QVariant(std::forward<Arg>(arg)));
  11. qDebug() << "enque task data is " << arg << endl;
  12. TaskEnque(task,std::forward<Args>(args)...);
  13. }
  14. template<typename Arg>
  15. void TaskEnque(std::shared_ptr<LogTask> task, Arg&& arg){
  16. task->_logdatas.enqueue(QVariant(std::forward<Arg>(arg)));
  17. qDebug() << "enque task data is " << arg << endl;
  18. }
  19. //可变参数列表,异步写
  20. template<typename... Args>
  21. void AsyncWrite(LogLv level, Args&&... args) {
  22. auto task = std::make_shared<LogTask>();
  23. //折叠表达式依次将可变参数写入队列,需C++17版本支持
  24. (task->_logdatas.enqueue(QVariant(std::forward<Args>(args))), ...);
  25. //如不支持C++17 请用这个版本入队
  26. //TaskEnque(task, args...);
  27. task->_level = level;
  28. std::unique_lock<std::mutex> lock(_mtx);
  29. _queue.enqueue(task);
  30. bool notify = (_queue.size() == 1)?true:false;
  31. lock.unlock();
  32. // 通知等待的线程有新的任务可处理
  33. if(notify){
  34. _empty_cond.notify_one();
  35. }
  36. }
  37. void Stop(){
  38. _b_stop = true;
  39. _empty_cond.notify_one();
  40. }
  41. private:
  42. AsyncLog(const AsyncLog&) = delete;
  43. AsyncLog& operator = (const AsyncLog&) = delete;
  44. AsyncLog(QObject* parent = nullptr):QThread(parent),_b_stop(false){
  45. }
  46. void run(){
  47. for(;;){
  48. std::unique_lock<std::mutex> lock(_mtx);
  49. while(_queue.empty() && !_b_stop){
  50. _empty_cond.wait(lock);
  51. }
  52. if(_b_stop){
  53. return;
  54. }
  55. auto logtask = _queue.dequeue();
  56. lock.unlock();
  57. processTask(logtask);
  58. }
  59. }
  60. void processTask(std::shared_ptr<LogTask> task){
  61. qDebug() << "log level is " << task->_level << endl;
  62. if(task->_logdatas.empty()){
  63. return;
  64. }
  65. // 队列首元素
  66. auto head = task->_logdatas.dequeue();
  67. auto formatstr = head.toString().toStdString();
  68. for(;!(task->_logdatas.empty());){
  69. auto data = task->_logdatas.dequeue();
  70. qDebug() << "deque task data is " << data;
  71. formatstr=formatString(formatstr, data);
  72. }
  73. qDebug() << "formatstr is " << QString::fromStdString(formatstr) << endl;
  74. }
  75. template<typename... Args>
  76. std::string formatString(const std::string& format, Args&&... args) {
  77. std::string result = format;
  78. size_t pos = 0;
  79. //lambda表达式查找并替换字符串
  80. auto replacePlaceholder = [&](const std::string& placeholder, const std::string& replacement) {
  81. size_t placeholderPos = result.find(placeholder, pos);
  82. if (placeholderPos != std::string::npos) {
  83. result.replace(placeholderPos, placeholder.length(), replacement);
  84. pos = placeholderPos + replacement.length();
  85. }else{
  86. result = result + " " + replacement;
  87. }
  88. };
  89. (replacePlaceholder("{}", QVariant(std::forward<Args>(args)).toString().toStdString()), ...);
  90. qDebug() << "result is : " << QString::fromStdString(result) << endl;
  91. return result;
  92. }
  93. std::condition_variable _empty_cond;
  94. QQueue<std::shared_ptr<LogTask> > _queue;
  95. bool _b_stop;
  96. std::mutex _mtx;
  97. };

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

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

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

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

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

  1. (表达式, ...)

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

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

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

测试和使用

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

  1. int main(int argc, char *argv[])
  2. {
  3. QApplication a(argc, argv);
  4. auto & instance = AsyncLog::AsyncLog::Instance();
  5. instance.AsyncWrite(AsyncLog::LogLv::ERRORS, "hello", "world !",
  6. "my name is {} ", "zack {}", "fair");
  7. instance.start();
  8. MainWindow main;
  9. main.show();
  10. a.exec();
  11. qDebug() << "Application Quitting, Please Wait...";
  12. AsyncLog::AsyncLog::Instance().Stop();
  13. // 暂停执行500毫秒(0.5秒)
  14. std::this_thread::sleep_for(std::chrono::milliseconds(500));
  15. qDebug() << "Application exited...";
  16. return 0;
  17. }

函数运行后输出如下

  1. formatstr is "hello world ! my name is zack fair "
  2. Application Quitting, Please Wait...
  3. Application exited...

总结

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

QT版本

C++版本

热门评论

热门文章

  1. C++ 类的继承封装和多态

    喜欢(588) 浏览(5018)
  2. windows环境搭建和vscode配置

    喜欢(587) 浏览(2839)
  3. Linux环境搭建和编码

    喜欢(594) 浏览(12319)
  4. 解密定时器的实现细节

    喜欢(566) 浏览(3500)
  5. slice介绍和使用

    喜欢(521) 浏览(2495)

最新评论

  1. 聊天项目(9) redis服务搭建 pro_lin:redis线程池的析构函数,除了pop出队列,还要free掉redis连接把
  2. 答疑汇总(thread,async源码分析) Yagus:如果引用计数为0,则会执行 future 的析构进而等待任务执行完成,那么看到的输出将是 这边应该不对吧,std::future析构只在这三种情况都满足的时候才回block: 1.共享状态是std::async 创造的(类型是_Task_async_state) 2.共享状态没有ready 3.这个future是共享状态的最后一个引用 这边共享状态类型是“_Package_state”,引用计数即使为0也不应该block啊
  3. C++ 并发三剑客future, promise和async Yunfei:大佬您好,如果这个线程池中加入的异步任务的形参如果有右值引用,这个commit中的返回类型推导和bind绑定就会出现问题,请问实际工程中,是不是不会用到这种任务,如果用到了,应该怎么解决?
  4. Qt MVC结构之QItemDelegate介绍 胡歌-此生不换:gpt, google

个人公众号

个人微信