std :: cerr不等待std :: cout(运行CTest时)

时间:2015-06-02 10:17:44

标签: c++ output io-redirection ctest

上下文

我为用户写了一个记录器打印消息。具有级别“debug”,“info”或“warning”的消息将打印在std::cout中,级别为“error”或“system_error”的消息将打印在std::cerr中。我的程序不是多线程的。我使用gcc 4.7.2和CMake 3.1.0在Linux openSUSE 12.3下工作。

我的问题

我发现有时,当一条错误消息(在std::cerr中打印)跟随长信息消息(打印在std::cout中)并且输出被重定向到文件LastTest.log时CTest,错误消息出现在信息消息中(请参见下面的示例)。我不太了解这种行为,但我想为std::cout启动了一个写线程,然后代码继续,另一个写入线程为std::cerr推出,而不等待第一个终止。

如果不仅使用std::cout

,是否可以避免这种情况

我在终端没有问题。只有当CTest将输出重定向到LastTest.log文件时才会发生。

请注意我的缓冲区已刷新。拨打std::endl之后,std::cerr不会出现问题!

实施例

预期行为:

[ 12:06:51.497   TRACE ] Start test
[ 12:06:52.837 WARNING ] This
                         is
                         a
                         very
                         long
                         warning
                         message...
[ 12:06:52.837   ERROR ] AT LINE 49 : 7
[ 12:06:52.841   ERROR ] AT LINE 71 : 506
[ 12:06:52.841   TRACE ] End of test

会发生什么:

[ 12:06:51.497   TRACE ] Start test
[ 12:06:52.837 WARNING ] This
                         is
                         a
                         very
                         long
[ 12:06:52.837   ERROR ] AT LINE 49 : 7
                         warning
                         message...
[ 12:06:52.841   ERROR ] AT LINE 71 : 506
[ 12:06:52.841   TRACE ] End of test

我如何调用我的记录器

以下是我使用记录器调用std::coutstd::cerr的示例。 我用这样的maccros调用记录器:

#define LOG_DEBUG(X) {if(Log::debug_is_active()){std::ostringstream o;o<<X;Log::debug(o.str());}}
#define LOG_ERROR(X) {if(Log::error_is_active()){std::ostringstream o;o<<X;Log::error(o.str());}}
//...
LOG_DEBUG("This" << std::endl << "is" << std::endl << "a message");
LOG_ERROR("at line " << __LINE__ << " : " << err_id);

void Log::debug(const std::string& msg)
{
    Log::write_if_active(Log::DEBUG, msg);
}
void Log::error(const std::string& msg)
{
    Log::write_if_active(Log::ERROR, msg);
}
//...
void Log::write_if_active(unsigned short int state, const std::string& msg)
{
    Instant now;
    now.setCurrentTime();
    std::vector<std::string> lines;
    for(std::size_t k = 0; k < msg.size();)
    {
        std::size_t next_endl = msg.find('\n', k);
        if(next_endl == std::string::npos)
            next_endl = msg.size();
        lines.push_back(msg.substr(k, next_endl - k));
        k = next_endl + 1;
    }
    boost::mutex::scoped_lock lock(Log::mutex);
    for(unsigned long int i = 0; i < Log::chanels.size(); ++i)
        if(Log::chanels[i])
            if(Log::chanels[i]->flags & state)
                Log::chanels[i]->write(state, now, lines);
}

这里,log chanel是专用于终端输出的对象,写入功能是:

void Log::StdOut::write(unsigned short int state, const Instant& t, const std::vector<std::string>& lines)
{
    assert(lines.size() > 0 && "PRE: empty lines");
    std::string prefix =  "[ ";
    if(this->withDate || this->withTime)
    {
        std::string pattern = "";
        if(this->withDate)
            pattern += "%Y-%m-%d ";
        if(this->withTime)
            pattern += "%H:%M:%S.%Z ";
        prefix += t.toString(pattern);
    }
    std::ostream* out = 0;
    if(state == Log::TRACE)
    {
        prefix += "  TRACE";
        out = &std::cout;
    }
    else if(state == Log::DEBUG)
    {
        prefix += "  DEBUG";
        out = &std::cout;
    }
    else if(state == Log::INFO)
    {
        prefix += "   INFO";
        out = &std::cout;
    }
    else if(state == Log::WARNING)
    {
        prefix += "WARNING";
        out = &std::cout;
    }
    else if(state == Log::ERROR)
    {
        prefix += "  ERROR";
        out = &std::cerr;
    }
    else if(state == Log::SYS_ERROR)
    {
        prefix += "SYERROR";
        out = &std::cerr;
    }
    else
        assert(false && "PRE: Invalid Log state");
    prefix += " ] ";
    (*out) << prefix << lines[0] << "\n";
    prefix = std::string(prefix.size(), ' ');
    for(unsigned long int i = 1; i < lines.size(); ++i)
        (*out) << prefix << lines[i] << "\n";
    out->flush();
}

您可以看到执行日志指令时刷新了我的缓冲区。

3 个答案:

答案 0 :(得分:20)

我之前以一些形式看过这种行为。中心思想是记住std::coutstd::cerr写入两个完全独立的流,所以每当你在同一个地方看到两者的输出时,都是因为某些程序之外的机制,它合并了两个流。

有时,我认为这只是因为一个错误,例如

myprogram > logfile &
tail -f logfile

正在查看写入的日志文件,但忘记将stderr重定向到日志文件,因此写入stdout之前至少要经过两个额外的缓冲层tail显示,但写入stderr直接转到tty,因此可以混入。

我见过的其他例子涉及合并流的外部流程。我对CTest一无所知,但也许它正在这样做。这些进程没有义务按照您最初将它们写入流的确切时间对行进行排序,即使他们想要也可能无法访问该信息!

你真的只有两个选择:

  • 将两个日志写入同一个流 - 例如使用std::clog代替std::coutstd::cout代替std::cerr;或使用myprogram 2>&1或类似的
  • 启动该计划
  • 确保合并是由一个实际知道它正在合并的过程完成的,并且需要适当地进行合并。如果您通过传递包含日志记录事件的数据包进行通信而不是自己编写格式化的日志消息,则可以更好地工作。

答案 1 :(得分:12)

回答我自己的问题

最后,它来自bug in CMake

CTest无法管理两个缓冲区的顺序,因此不关心输出的确切顺序。

将在CMake&gt; = 3.4中解决。

答案 2 :(得分:2)

我不是C ++专家,但这可能会有所帮助......

我相信当你重定向到文件时,你在这里看到的问题是由cstdio库试图变得聪明造成的。我的理解是,在Linux上,C ++ iostream最终将其输出发送到cstdio库。

启动时,cstdio库会检测您是将输出发送到终端还是文件。如果输出到达终端,那么stdio是行缓冲的。如果输出转到文件,则stdio将变为块缓冲。

从未缓冲输出到stderr,因此将立即发送。

对于解决方案,您可以尝试在stdout上使用fflush,或者您可以考虑在stdout上使用setvbuf函数来强制行缓冲输出(如果您愿意,甚至可以使用无缓冲输出)。像这样的东西应该强制stdout行缓冲setvbuf(stdout, NULL, _IOLBF, 0)