在多线程中登录的良好做法是什么?

What's the good practice of logging in multithreading?

本文关键字:是什么 多线程 登录      更新时间:2023-10-16

我正在研究一个多线程c++代码,它可以作为exe或Excel的XLL附加组件运行,在任何一种情况下,它都将在多个线程中运行。

做日志记录的好习惯是什么?

在c#程序中有一个类似的线程,在多线程环境中进行日志记录和测试,建议:

将日志实例封装在线程安全的单例中。不要使用再次检查锁定!此外,使用日志记录可能是有意义的5.

我想知道这是否仍然适用于c++程序?

有人说Singleton是邪恶的,但似乎登录是这种模式的有效案例?

有哪些好的日志库可以处理流缓冲区问题,所以通过日志文件就足以重建可信的场景?

使用Singleton从多个线程进行日志记录,这会降低性能吗?必须使用互斥锁吗?

我在Windows环境下工作。

如何在不使用单例的情况下从多线程进行日志记录?

我觉得这个问题很奇怪。

访问基于ram的日志(循环方式)的关键部分是:


  • 我经常在一些类实例的构造函数中创建日志,这是正在发生的事情的中心。一旦创建,任何线程都可以通过该名称访问日志。在下面的例子中,"M_dtbLog".

    if (0 == M_dtbLog){M_dtbLog = new DTB::Log(1*60* 1000,1024 *256*2);dtbAssert(0 != M_dtbLog);}

update 10/06 10:45 -忘了说这个博客是在2011年之前创建的,所以没有std::线程。线程为Posix线程。

  • 在您希望访问日志的任何代码中:

1)拉入代码

#include "../../bag/src/dtb_log.hh"

2)当时,为了方便起见,我添加了一个宏(如果必须的话)。说明:

//     several years ago (in a much more c-style approach),
#define  DBG  if(M_dtbLog)  (void)M_dtbLog->enq

2b)那时我还加了:

// the compiler checks many more things for printf does
// use this simple #define temporarily to test your DBG's
#if (0)
#undef  DBG
#define DBG if(M_dtbLog) (void)::printf
#endif

3)"DBG"作为代码注入器现在是可用的,并且每次调用都确认该队列是可用的。例子:

void LMBM::Node:init(void)
{
if(m_next)
m_next->init(); 
//...
DBG("m_semIn %2d = %pn", m_nodeId, m_semIn);  // <<<<<<<<

忘记提到这些节点的每个都有自己的线程和输入信号量,因此是静态的M_dtbLog。默认设置创建10个节点(以及10个线程和信号量)。每个节点都可以直接访问静态M_dtbLog。4)最近,我使用我认为更简单的c++方法…以后再讨论。

5)有时候越简单越好,特别是对于单元测试。这里我有时使用std::stringstream(这不是线程安全的)。只需要记住在任务结束时限制进入代码的数量。

6)对于生产,当不需要Log时,如果不需要(通常不需要),我就不实例化日志。但是我把DBG语句留在代码中,以便在需要调试时使用。


在任何想要使用此日志的文件/类中,无论哪个线程,只需添加#include, #define和您想要的任意数量的DBG。

目前不支持度量性能问题。它正在写入内存,对于我的目的来说已经足够快了。


独占访问控制(用于多线程访问)在dtb_log实现中。

在我的Linux版本的这段代码中,我在本地模式下使用Posix进程信号量(即不是进程共享,不是互斥锁,也不是std::互斥锁)。

构造字符串和预挂起时间和线程id等一般工作是在自动内存(通常称为堆栈)中完成的,因此线程(每个线程都有自己的堆栈)不会相互干扰。

自动内存中的字符串然后进入轮询内存缓冲区。(使用私有方法DTB::Log::enqRrq()) dtb_log可以被任何线程使用。临界截面以典型的方式使用。'enqRrq'大约是12行代码,只是将字符从一个buff洗牌到循环buff。

update 10/6 -代码片段-


紧跟代码注入时间戳、线程id和消息等

// buff 1 now ready to install
enqRrq(buff1, retVal); // install text into round-robin queue

接下来,将新的日志条目转移到轮循缓冲区

void DTB::Log::enqRrq(const char* buff, size_t sz)
{
DTB::ProcessSemCritSect critSect(m_mutex);
dtbAssert(0 == m_rrBuff[m_nextIn]); // code broken
// move char from buff into m_rrBuff
{
{
bmpNextIn(); // leave one null between records
for (size_t i = 0; i < sz; i += 1) // insert sz chars of buff
{
m_rrBuff[m_nextIn] = buff[i];
bmpNextIn();                     // m_rrBuff is round-robin
}
m_rrBuff[m_nextIn] = 0; // insert null char at end of m_rrBuff
}
}
uint32_t lni = m_nextIn + 1; // lni starts after null we just inserted
if (lni >= m_rrBuffSize) lni = 0; // round-robin
// stomp 0's to end of oldest message
{
while (0 != m_rrBuff[lni]) // while in middle of oldest message
{
m_rrBuff[lni++] = 0; // stomp on oldest message (to its end)
if (lni >= m_rrBuffSize) lni = 0; // round-robin
}
}
}

我碰巧发现了几个这种日志输出的例子。对不起,行长问题,最大的是160个字符。

请注意,这是我找到的日志的最后5秒。

  • retrieve_shelf_inventory()显然是每秒一次。(通常重复的日志条目会被用户(而不是日志)抑制,因为它们会围绕并践踏您想要看到的内容)

  • 最后一秒显示一些led和办公室警报的更新。

  • pem_cm,一个不同的代码层,[线程块],正在发送触发这些日志条目的命令。

  • 这个系统大约有1000个线程在运行。
    和100个左右的开发人员整理代码。

    05:27:41 0x010117 262.816 McU_t::retrieve_shelf_inventory() size=9105:27:41 0x010117 262.816—mcu:sdd_io2(ant=210 (SDD_GET_SHELF_PHY_INV) sender=0001020e (pem_cm), sec_id=4088C04080000000) eqType=35 major= 1,519, (1109792 us)

    05:27:42 0x010117 263.928 mc_t::retrieve_shelf_inventory() size=9105:27:42 0x010117 263.928—mcu:sdd_io2(ant=210 (SDD_GET_SHELF_PHY_INV) sender=0001020e (pem_cm), sec_id=4088C04080000000) eqType=35 major= 1,520, (1111872 us)

    05:27:43 0x010117 265.040 McU_t::retrieve_shelf_inventory() size=9105:27:43 0x010117 265.040—mcu:sdd_io2(ant=210 (SDD_GET_SHELF_PHY_INV) sender=0001020e (pem_cm), sec_id=4088C04080000000) eqType=35 major= 1,521, (1111873 us)

    05:27:44 0x010117 266.152 mc_t::retrieve_shelf_inventory() size=9105:27:44 0x010117 266.152—mcu:sdd_io2(ant=210 (SDD_GET_SHELF_PHY_INV) sender=0001020e (pem_cm), sec_id=4088C04080000000) eqType=35 major= 1,522, (1111883 us)

    05:27:44 0x010117 266.152 McU_t::sdd_control_led(indx=13, color=1, action=1) = 005:27:44 0x010117 266.152—mcu:sdd_io2(ant=20 (SDD_LED_CONTROL) sender=0001020e (pem_cm), sec_id=4088C04080000000) eqType=35 major= 1,523, (48 us)

    05:27:44 0x010117 266.152 McU_t::sdd_control_led(indx=14, color=1, action=1) = 005:27:44 0x010117 266.152—mcu:sdd_io2(ant=20 (SDD_LED_CONTROL) sender=0001020e (pem_cm), sec_id=4088C04080000000) eqType=35 major= 1,524, (19 us)

    05:27:44 0x010117 266.152 McU_t::sdd_control_led(indx=15, color=2, action=1) = 005:27:44 0x010117 266.152—mcu:sdd_io2(ant=20 (SDD_LED_CONTROL) sender=0001020e (pem_cm), sec_id=4088C04080000000) eqType=35 major= 1,525, (19 us)

    05:27:44 0x010117 266.152 McU_t::sdd_control_led(indx=12, color=0, action=0) =005:27:44 0x010117 266.152—mcu:sdd_io2(ant=20 (SDD_LED_CONTROL) sender=0001020e (pem_cm), sec_id=4088C04080000000) eqType=35 major= 1,526, (21 us)

    05:27:44 0x010117 266.152 pci2:blsr:office_alarms:change() aud_cr . vis_cr . aud_mn . vis_mj . vis_mn . aud_mn。vis_MN 0000003 f05:27:44 0x010117 266.152—mcu:sdd_io2(ant=35 (SDD_SET_ALARM) sender=0001020e (pem_cm), sec_id=4088C04080000000) eqType=35 major= 1,527, (26 us)

    05:27:44 0x010117 266.200 McU_t::sdd_control_led(indx=13, color=1, action=1) = 005:27:44 0x010117 266.200—mcu:sdd_io2(ant=20 (SDD_LED_CONTROL) sender=0001020e (pem_cm), sec_id=4088C04080000000) eqType=35 major= 1,528, (49 us)

    05:27:44 0x010117 266.200 McU_t::sdd_control_led(indx=14, color=1, action=1) = 005:27:44 0x010117 266.200—mcu:sdd_io2(ant=20 (SDD_LED_CONTROL) sender=0001020e (pem_cm), sec_id=4088C04080000000) eqType=35 major= 1,529, (20 us)

    05:27:44 0x010117 266.200 McU_t::sdd_control_led(indx=15, color=2, action=0) =005:27:44 0x010117 266.200—mcu:sdd_io2(ant=20 (SDD_LED_CONTROL) sender=0001020e (pem_cm), sec_id=4088C04080000000) eqType=35 major= 1,530, (19 us)

    05:27:44 0x010117 266.200 McU_t::sdd_control_led(indx=12, color=0, action=0) =005:27:44 0x010117 266.200—mcu:sdd_io2(ant=20 (SDD_LED_CONTROL) sender=0001020e (pem_cm), sec_id=4088C04080000000) eqType=35 major= 1,531, (20 us)

    05:27:44 0x010117 266.200 pci2:blsr:office_alarms:change() aud_cr . vis_cr .aud_mn. vis_mj . vis_mn .aud_mn。vis_mn 0000001 b05:27:44 0x010117 266.200—mcu:sdd_io2(ant=35 (SDD_SET_ALARM) sender=0001020e (pem_cm), sec_id=4088C04080000000) eqType=35 major= 1,532, (25 us)

    513行,55287字节(m_size=65536)显示,55287年


update 11/04

有人说单例是邪恶的,但似乎登录是一个有效的案例这种模式吗?

我已经使用了几个单例,但最终发现单例没有增加任何价值。它确实有一个令人难忘的名字,所以更容易记住,所以可能对其他人来说更容易读懂,所以你比其他模式看到更多(我认为)。但是,即使当我使用这些想法来访问和控制一个只有一个"如何"的方法时……在我看来,单例并没有简化或加快任何事情。

我还没有看到单例"提供"线程安全的代码。我相信模式手册(发布了该模式)有一个声明,本质上说,没有一个模式是线程安全的。(但我已经有一段时间没看了。)也许c#的单例是不同的。

当你引用的作者说,"在一个线程安全的单例中包装你的日志实例",我相信他的意思不过是a)使用一个单例,b)在单例中添加线程安全。单例不提供线程安全,而线程安全也不提供单例特性。


更新11/04

使用Singleton从多个线程进行日志记录,将会这样降低性能?必须使用互斥锁吗?

正如我上面所说的,我没有看到单例提供线程安全的想法。我不喜欢。

当多个线程同步访问共享资源时,性能总是会受到影响。上面的代码使用POSIX信号量创建了一个临界区。该实现看起来像:
DTB::ProcessSemCritSect::ProcessSemCritSect(ProcessSemaphore*  a_processSem) :
m_processSem (a_processSem)
{
assert(nullptr != m_processSem);
int wait_status = m_processSem->wait();  // block if already locked
assert(DTB::ProcessSemaphore::SUCCESS == wait_status);
}

DTB::ProcessSemCritSect::~ProcessSemCritSect(void)
{
assert(nullptr != m_processSem);
int post_status = m_processSem->post(); // un-block any waiting thread
assert(DTB::ProcessSemaphore::SUCCESS == post_status);
m_processSem = 0;
}

我的基准测试表明,在我的系统上,两步wait()和post()非常快<25 ns。

如果你的线程试图使用日志有最小的冲突,这将是信号量开销的下限(在我的系统上)。

在我的系统中,在这种类型的临界区中发生碰撞的线程(可能在不同的内核中)可能会花费多达12,000 ns的时间进行上下文切换。你的表现会有所不同。