C/C++中的并发日志文件访问

Concurrent log file access in C/C++

本文关键字:日志 文件 访问 并发 C++      更新时间:2023-10-16

我正在创建一个多线程程序,几个线程可能需要调用全局函数

writeLog(const char* pMsg);

writeLog将实现类似tihs:的功能

void writeLog(const char* pMsg)
{
   CRITICAL_SECTION cs;
   // initialize critical section
   ...
   EnterCriticalSection(&cs);
   // g_pLogFilePath is a global variable.
   FILE *file;
   if (0!=fopen_s(&file, g_pLogFilePath, "r+"))  
      return;
   fprintf(file, pMsg);
   fclose(file):
   LeaveCriticalSection(&cs);
}

我的问题是:

1) is it the best way to do concurrent logging? i.e., using critical section.
2) since I will write log in many places in the threads, 
and since each log writing will involve open/close file,
does the io will impact the performance significantly?

谢谢!

执行并发日志记录的最佳方法是使用C++的现有日志库之一。它们有许多您可能想要使用的功能(不同的附加器、格式化、并发等)。

如果你仍然想有自己的解决方案,你可能会有这样的解决方案:初始化一次并保持状态(文件处理程序和互斥对象)的简单单例

class Log
{
public:
    // Singleton
    static Log & getLog() 
    {
        static Log theLog;
        return theLog;
    }
    void log(const std::string & message)
    {
         // synchronous writing here
    }
private:
    // Hidden ctor
    Log()
    {
         // open file ONCE here
    }
    // Synchronisation primitive - instance variable
    // CRITICAL_SECTION or Boost mutex (preferable)
    CRITICAL_SECTION cs_;
    // File handler: FILE * or std::ofstream
    FILE * handler_;
};

回答您的问题:

  1. 是的,并发日志记录确实需要一个关键部分。

  2. 是的,日志记录确实会影响性能。

如注释中所述,用于"保护"关键部分的对象必须可由所有线程访问,例如全局变量或singleton。

关于日志记录性能,IO可能成本高昂。一种常见的方法是使用一个日志记录对象来缓冲要记录的消息,并且只在缓冲区已满时进行写入。这将有助于提高性能。此外,考虑具有几个日志级别:DEBUG、INFO、WARNING、ERROR。

CS是保护日志记录的合理方法,是的。为了避免对每个线程的每个调用都施加open/write/close,通常将字符串(如果还没有mallocated/newed,则可能需要将其复制)排队到一个单独的日志线程。阻塞磁盘延迟随后从日志记录调用中得到缓冲。任何延迟写入等优化都可以在日志线程中实现。

或者,正如其他海报所建议的那样,只需使用一个已经实现了所有这些东西的日志框架。

我正在写一个答案,然后一个断路器跳闸了。既然我的答案还在草稿中,我不妨继续下去。与提供singleton类的答案大致相同,但我做得更像C。这一切都在一个单独的源文件中(例如Logging.cpp)。

static CRITICAL_SECTION csLogMutex;
static FILE *fpFile = NULL;
static bool bInit = false;
bool InitLog( const char *filename )
{
    if( bInit ) return false;
    bInit = true;
    fpFile = fopen( filename, "at" );
    InitializeCriticalSection(&csLogMutex);
    return fpFile != NULL;
}
void ShutdownLog()
{
    if( !bInit ) return;
    if( fpFile ) fclose(fpFile);
    DeleteCriticalSection(&csLogMutex);
    fpFile = NULL;
    bInit = false;
}

这些是在您的应用程序入口/出口中调用的。。。至于日志记录,我更喜欢使用变量参数列表,这样我就可以进行printf风格的日志记录。

void writeLog(const char* pMsg, ...)
{
   if( fpFile == NULL ) return;
   EnterCriticalSection(&csLogMutex);
   // You can write a timestamp into the file here if you like.
   va_list ap;
   va_start(ap, pMsg);
   vfprintf( fpFile, pMsg, ap );
   fprintf( fpFile, "n" );        // I hate supplying newlines to log functions!
   va_end(ap);
   LeaveCriticalSection(&csLogMutex);
}

如果您计划在DLL中进行日志记录,则不能使用这种静态方法。相反,您需要使用_fsopen打开文件并拒绝读/写共享。

如果您希望应用程序崩溃,您可能也希望定期调用fflush。或者,如果你想从外部实时监控日志,你每次都必须调用它。

是的,关键部分会对性能产生影响,但与写入文件的性能成本相比,这算不了什么。您可以每秒进入关键部分数千次,而无需担心。