C++ 中的异步线程安全日志记录(无互斥体)

Posted

技术标签:

【中文标题】C++ 中的异步线程安全日志记录(无互斥体)【英文标题】:Asynchronous thread-safe logging in C++ (no mutex) 【发布时间】:2011-11-16 10:36:24 【问题描述】:

我实际上正在寻找一种在我的 C++ 中进行异步和线程安全日志记录的方法。

我已经探索过线程安全的日志记录解决方案,例如 log4cpp、log4cxx、Boost:log 或 rlog,但似乎它们都使用互斥锁。据我所知,互斥锁是一种同步解决方案,这意味着所有线程在尝试写入消息时都会被锁定,而其他线程则会锁定。

你知道解决办法吗?

【问题讨论】:

如果您在写入日志文件时不使用互斥锁,则可能会出现崩溃或混合日志,因为您的写入访问是同时进行的 【参考方案1】:

我认为您的说法是错误的:使用互斥锁不一定等同于同步解决方案。是的,Mutex 用于同步控制,但它可以用于许多不同的事情。例如,我们可以在生产者消费者队列中使用互斥锁,而日志记录仍在异步发生。

老实说,我还没有研究过这些日志库的实现,但是制作一个异步 appender(对于 log4j 之类的 lib)应该是可行的,该 appender 记录器写入生产者消费者队列,另一个工作线程负责写入文件(甚至委托给另一个附加程序),以防未提供。


编辑: 刚刚在 log4cxx 中进行了简短的扫描,它确实提供了一个 AsyncAppender,它执行我的建议:缓冲传入的日志事件,并异步委托给附加的 appender。

【讨论】:

+1 用于强调在推送指针/引用所需的短时间内锁定队列与锁定完整的磁盘写入操作之间的巨大性能差异。【参考方案2】:

我建议通过仅使用一个线程进行日志记录来避免该问题。为了将必要的数据传递到日志,您可以使用无锁 fifo 队列(线程安全,只要生产者和消费者严格分开并且每个角色只有一个线程 - 因此每个生产者都需要一个队列。)

包含快速无锁队列示例:

队列.h:

#ifndef QUEUE_H
#define QUEUE_H

template<typename T> class Queue

public:
    virtual void Enqueue(const T &element) = 0;
    virtual T Dequeue() = 0;
    virtual bool Empty() = 0;
;

hybridqueue.h:

#ifndef HYBRIDQUEUE_H
#define HYBRIDQUEUE_H

#include "queue.h"


template <typename T, int size> class HybridQueue : public Queue<T>


public:
    virtual bool Empty();
    virtual T Dequeue();
    virtual void Enqueue(const T& element);
    HybridQueue();
    virtual ~HybridQueue();

private:
    struct ItemList
    
        int start;
        T list[size];
        int end;
        ItemList volatile * volatile next;
    ;

    ItemList volatile * volatile start;
    char filler[256];
    ItemList volatile * volatile end;
;

/**
 * Implementation
 * 
 */

#include <stdio.h>

template <typename T, int size> bool HybridQueue<T, size>::Empty()

    return (this->start == this->end) && (this->start->start == this->start->end);


template <typename T, int size> T HybridQueue<T, size>::Dequeue()

    if(this->Empty())
    
        return NULL;
    
    if(this->start->start >= size)
    
        ItemList volatile * volatile old;
        old = this->start;
        this->start = this->start->next;
            delete old;
    
    T tmp;
    tmp = this->start->list[this->start->start];
    this->start->start++;
    return tmp;


template <typename T, int size> void HybridQueue<T, size>::Enqueue(const T& element)

    if(this->end->end >= size) 
        this->end->next = new ItemList();
        this->end->next->start = 0;
        this->end->next->list[0] = element;
        this->end->next->end = 1;
        this->end = this->end->next;
    
    else
    
        this->end->list[this->end->end] = element;
        this->end->end++;
    


template <typename T, int size> HybridQueue<T, size>::HybridQueue()

    this->start = this->end = new ItemList();
    this->start->start = this->start->end = 0;


template <typename T, int size> HybridQueue<T, size>::~HybridQueue()




#endif // HYBRIDQUEUE_H

【讨论】:

+1:无锁队列和读取线程确实是要走的路 什么?每个生产者一个队列 - 如何管理?无论如何,这个队列是如何工作的——当队列为空时,生产者在等待什么?一个记录器线程、一个 P-C 队列和一个仅围绕队列推送的锁(即队列仅在推送一个对象实例(即 32/64 位参考值)时锁定,对我来说似乎更干净. 那个!您可以拥有这些队列的向量或映射,您可以在其中将队列分配给线程,消费者只需遍历整个队列数组。生产者不等待,生产者只是将更多数据排入队列:P 消费者可以休眠,做一些其他工作,或者 OP 可以在推送时引入一些信号机制,类可以很容易地修改。至于锁定,OP 想避免这种情况。 不管怎样,锁机制可以很容易地引入,但是代码本身不能支持多个生产者在没有任何锁的情况下推入一个队列。消费者遍历整个队列池为模型引入了公平性。 好吧,也许只有我一个人,但是一个必须在每个线程的基础上管理的排队系统似乎非常混乱,因为它可以避免可能锁定 32/64 的队列推送位项目。无论如何,使用 condvar 或 semaphore 来表示消费者线程将成为内核调用,并且争用锁的可能开销不会产生太大影响。 IME,OP 希望避免琐碎的日志记录解决方案,其中互斥锁被锁定在完整的磁盘写入操作上,(因此争用的可能性很高)。【参考方案3】:

如果我的问题正确,您担心在记录器的关键部分执行 I/O 操作(可能写入文件)。

Boost:log 允许您定义自定义写入器对象。您可以定义 operator() 来调用异步 I/O 或将消息传递给您的日志线程(正在执行 I/O)。

http://www.torjo.com/log2/doc/html/workflow.html#workflow_2b

【讨论】:

【参考方案4】:

据我所知,没有图书馆会这样做 - 这太复杂了。您必须自己动手,这是我刚刚想到的一个想法,创建一个每个线程的日志文件,确保每个条目中的第一项是时间戳,然后在运行和排序之后合并日志(按时间戳) 以获取最终的日志文件。

您可以使用一些线程本地存储(比如FILE 句柄AFAIK,它不可能在线程本地存储中存储流对象)并在每个日志行上查找此句柄并写入特定的文件。

所有这些复杂性与锁定互斥锁?我不知道您的应用程序的性能要求,但如果它很敏感 - 为什么要记录(过度)?想想其他无需登录即可获得所需信息的方法?

另外要考虑的另一件事是尽可能少地使用互斥锁,即首先构建您的日志条目,然后在写入文件之前获取锁。

【讨论】:

【参考方案5】:

在 Windows 程序中,我们使用用户定义的 Windows 消息。首先,为堆上的日志条目分配内存。然后调用 PostMessage,指针为 LPARAM,记录大小为 WPARAM。接收器窗口提取记录、显示并将其保存在日志文件中。然后 PostMessage 返回,分配的内存被发送者释放。这种方法是线程安全的,您不必使用互斥锁。并发由 Windows 的消息队列机制处理。 不是很优雅,但很有效。

【讨论】:

windows消息队列是否真的保证了它的并发处理方案?我敢打赌它不是无锁的。 它是无锁的,也可能不是。更重要的是,与任何“普通”用户空间 P-C 队列相比,它的速度都很慢(例如,具有 CS 保护它的队列和用于阻塞的信号量)。 OTOH,比 WMQ 更糟糕 - IOCP 队列更慢!至少 WMQ 是比模板化(读作“过度且可避免的数据复制”)更好的解决方案,每个生产者线程的无锁队列都必须由消费者轮询。 @MartinJames 在陈述虚假事实之前先进行思考(读作“您可以模板化指向结构的指针”) @Erbureth - 是的,你当然可以这样做。如果我给人任何其他印象,那我不是故意的。不过,我要说的是,一些通用队列示例并没有这样做,保持锁的时间过长并增加了争用。在 OO 语言中,人们可能会认为泛型不是必需的,因为所有对象引用的大小都相同 :)【参考方案6】:

无锁算法不一定是最快的。定义你的界限。有多少个线程用于记录?一次日志操作最多会写入多少?

由于阻塞/唤醒线程,I/O 绑定操作比线程上下文切换慢得多。使用 10 个写线程的无锁/自旋锁算法会给 CPU 带来沉重的负担。

简而言之,在写入文件时阻塞其他线程。

【讨论】:

当您将日志条目推送到队列时,立即阻止其他线程 - 更好。

以上是关于C++ 中的异步线程安全日志记录(无互斥体)的主要内容,如果未能解决你的问题,请参考以下文章

C++多线程同步技巧--- 互斥体

在 C++ 中使用插槽和互斥体向量的线程的互斥插槽分配器

从 C++ 中的多个线程调用 Qt 中小部件类的信号函数是不是安全?

Linux线程安全篇

Linux线程安全篇Ⅰ

C++ 中的标准输出流是线程安全的(cout、cerr、clog)吗?