Подтвердить что ты не робот

Std:: cerr не ждет std:: cout (при запуске CTest)

Контекст

Я написал для пользователя сообщения для печати журнала. Сообщения с уровнем "debug", "info" или "warning" печатаются в std::cout, а сообщения с уровнем "error" или "system_error" печатаются в std::cerr. Моя программа не многопоточная. Я работаю под Linux openSUSE 12.3 с gcc 4.7.2 и CMake 3.1.0.

Моя проблема

Я обнаружил, что иногда, когда сообщение об ошибке (напечатанное в 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::cout или std::cerr с помощью моего регистратора. Я вызываю logger с такими макросами:

#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);
}

Здесь журнал-канал - это объект, выделенный для вывода терминала, функция записи:

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();
}

Вы можете видеть, что мой буфер сбрасывается при выполнении инструкции журнала.

4b9b3361

Ответ 1

Отвечая на мой вопрос

Наконец, он исходит из ошибка в CMake.

CTest не может управлять порядком двух буферов, а затем не заботится о точном порядке вывода.

Он будет разрешен в CMake >= 3.4.

Ответ 2

Я видел это поведение раньше в нескольких формах. Основная идея состоит в том, чтобы помнить, что std::cout и std::cerr записываются в два полностью отдельных потока, поэтому в любое время, когда вы видите вывод из одного и того же места, это связано с каким-то механизмом вне вашей программы, который объединяет два потока.

Иногда я вижу это просто из-за ошибки, например

myprogram > logfile &
tail -f logfile

который просматривает файл журнала по мере его написания, но забыл переадресовать stderr в лог файл тоже, поэтому запись в stdout проходит через, по крайней мере, два дополнительных уровня буферизации внутри tail, прежде чем отображать их, но записывает stderr перейти непосредственно к tty, и поэтому может быть смешанным.

Другие примеры, которые я видел, связаны с потоками слияния внешних процессов. Я ничего не знаю о CTest, но, возможно, он это делает. Такие процессы не обязаны сортировать строки точным временем, когда вы изначально пишете их в поток, и, вероятно, не имеете доступа к этой информации, даже если они хотели!


У вас действительно есть только два варианта:

  • Напишите оба журнала в тот же поток — например используйте std::clog вместо std::cout или std::cout вместо std::cerr; или запустить программу с помощью myprogram 2>&1 или аналогичного
  • Убедитесь, что слияние выполняется процессом, который действительно знает, что он объединяет, и позаботится сделать это соответствующим образом. Это работает лучше, если вы общаетесь, передавая пакеты, содержащие события журнала, вместо того, чтобы писать сами отформатированные сообщения журнала.

Ответ 3

Я не эксперт на С++, но это может помочь...

Я считаю, что проблема, которую вы видите здесь, при перенаправлении на файл, вызвана тем, что библиотека cstdio пытается быть умной. Я понимаю, что в Linux, iOS-потоки С++ в конечном итоге отправляют свой вывод в библиотеку cstdio.

При запуске библиотека cstdio обнаруживает, отправляет ли вы вывод на терминал или в файл. Если выход идет к терминалу, то stdio будет буферизироваться по строке. Если вывод идет в файл, то stdio становится блочным буфером.

Выход в stderr никогда не буферизуется, поэтому он будет отправлен немедленно.

Для решений, которые вы могли бы попробовать использовать fflush на stdout, или вы могли бы изучить возможность использования функции setvbuf на stdout для принудительного вывода строки буферизации (или даже небуферизованного вывода, если хотите). Что-то вроде этого должно заставить stdout быть привязанным к строке setvbuf(stdout, NULL, _IOLBF, 0).