2

C++ 中如何多线程写日志

 1 year ago
source link: https://blog.henix.info/blog/cpp-multithread-logging/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

C++ 中如何多线程写日志

最后更新日期:2022-07-03

  日志是几乎每个程序都需要的核心组件,用过 C++ 的 std::cout 的朋友都知道,它不是线程安全的!所以多个线程同时用 std::cout 输出的话,输出可能会错乱。有没有简单的解决方案?

  如果你在网上搜索,或者去看一些专门的日志库(如 glog)是如何解决这个问题的,不外乎这几种方法:

  1. 加一个全局锁
  2. 加一个多生产者-单消费者队列,其他线程写日志的时候先放入队列,单独开一个线程把队列里的日志一条一条写出来。

  第一种加锁的方案,增加了一个全局状态,我觉得不是很漂亮。方案二就更不用说了……我就是写个日志而已,有必要引入一个队列再加一个线程吗?有没有更简单的方案?

  我写 C++ 程序一般是不用 std::cout 来格式化输出的,而是用更底层的系统调用,也就是 write(2) 或 WriteFile / WriteConsole ,所以我们可以跳过 stdio / iostream 内部状态的同步问题,直接从操作系统层面考虑以下问题:

  如果有多个线程 / 进程同时调用 write(2) 或 WriteFile 写一个文件描述符或内核句柄,它们写入的内容会互相覆盖吗?

  用常识来想,操作系统层面难道没有任何机制来保证写操作的原子性吗?

  于是我们很自然地会问出以下问题:

  上述讨论的结论是:

  1. 对 Linux ,POSIX 规范保证用 O_APPEND 模式打开的文件,如果一次写入的内容不超过 PIPE_BUF(一般为 4096)字节,那么就是原子的1
  2. 对 Win32 的 WriteFile ,如果打开文件时添加了 FILE_APPEND_DATA 参数那么也可以保证追加操作是原子的

  所以我对于多线程写日志的解决方案是:每一行单独写入一个 buffer ,然后一次性调用 write(2) 写入,程序日志绝大多数情况下不会超过 PIPE_BUF 。

  如果你还在用 C/C++ 自带的 IO 函数,它们的内部存在我们无法控制的缓冲区(stdio buffering),这种方法不一定奏效。所以,直接用系统调用保平安。如果你还是想用 C/C++ 自带的格式化函数,一个简单的方法是先用 snprintf / sstream 把要输出的内容格式化到一个 buffer ,再用系统调用输出。

  一个极简的 C++ 11 线程安全日志库(POSIX only):

#include <sstream>
#include <iomanip>

#include <unistd.h>
#include <time.h>

/**
 * 在 out 后面追加时间戳,格式 YYYY-MM-DD HH:MM:SS.mmm
 */
void appendTimestampMs(std::ostream& out) {
    timespec t {};
    clock_gettime(CLOCK_REALTIME, &t); // 忽略错误
    {
        struct tm tm;
        localtime_r(&t.tv_sec, &tm);
        out << std::put_time(&tm, "%F %T");
    }
    char fill = out.fill();
    out << '.' << std::setfill('0') << std::setw(3) << t.tv_nsec / 1000000 << std::setfill(fill);
}

template<class... Args>
void plog(Args&&... args) {
    using _expander = int[];
    std::stringstream buf;
    // 先输出时间,精确到毫秒
    appendTimestampMs(buf);
    buf << ' ';
    (void)_expander{ (void(buf << std::forward<Args>(args)), 0)... };
    buf << '\n';
    std::string str = buf.str();
    write(STDOUT_FILENO, str.data(), str.size());
}
plog("[INFO] test: ", 10);
2022-05-06 20:47:43.725 [INFO] test: 10

  1. 引用自 write(2) 手册页:“If the file was open(2)ed with O_APPEND, the file offset is first set to the end of the file before writing. The adjustment of the file offset and the write operation are performed as an atomic step.”↩︎

评论邮箱 评论帮助
请按照如下格式发邮件:
[email protected]
[复制]
评论 / 回复内容,只支持纯文本

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK