qDebug epic fail: "мопед не мой, я просто разместил объяву"
От: slava_phirsov Россия  
Дата: 09.06.14 07:35
Оценка: 35 (2) :)
Доброго времени суток читающим!

Вот долго сомневался, куда писать: в КСВ, КУ, или сразу в Спортлото, но победил прагматизм — вдруг нижеприведенная инфа будет полезна кому-нибудь из Qt-шников не только для "чисто поржать", а еще может кто-то и дельный совет даст.

Как (должно быть) известно, qDebug() широко используется в Qt. А является ли эта функция реентерабельной? Документация ответа на этот вопрос не дает, значит, следует предполагать самое плохое, правда ведь? Я написал соответствующее замечание на bugtracker проекта. Как также (должно быть) известно, пользователь имеет возможность задать собственный обработчик отладочных сообщений — ну, например, чтобы снабжать каждое выводимое сообщение отметкой времени, или там раскрашивать его цветами кислотного трипа. Именно поэтому, по мнению товарища с говорящей, но труднопроизносимой фамилией Масиейра, которому предопределение предначертало отвечать на мое сообщение, нельзя давать никаких гарантий относительно реентерабельности qDebug(). Минуточку, мистер сисимаси, или как вас там, а как быть с тем, что QFile::open и QString::arg (это то, что я только, можно сказать, одной левой пяткой нарыл за пять минут) — реентерабельные, согласно документации — вызывают как бы не совсем реентерабельную qDebug()? Как это, миль, конечно, пардон, называется? Может быть, можно дать гарантии относительно реентерабельности хотя бы при отсутствии пользовательского обработчика отладочных сообщений?

I don't accept those use-cases. You're going to fix them before you release your product


("Мопед не мой, я просто разместил объяву") — ответил доблестный мистер масисиси. Вот ведь (чуть было слово б.дь не сказал)! Когда функция, которая, согласно документации, является реентерабельной, вызывает нереентерабельную qDebug — это, по утверждению доблестного мистера симасиси, оказывается, не их проблема, а моя. Вот ведь (опять чуть было слово б.дь не сказал)! Коммерческое софтостроение такое коммерческое софтостроение
Люди! Люди, смотрите, я сошел с ума! Люди! Возлюбите друг друга! (вы чувствуете, какой бред?)
qt qdebug facepalm
Re: qDebug epic fail: "мопед не мой, я просто разместил объяву"
От: visual_wind  
Дата: 09.06.14 14:24
Оценка: 21 (2)
Здравствуйте, slava_phirsov, Вы писали:
[...]
_>Минуточку, мистер сисимаси, или как вас там, а как быть с тем, что QFile::open и QString::arg (это то, что я только, можно сказать, одной левой пяткой нарыл за пять минут) — реентерабельные, согласно документации — вызывают как бы не совсем реентерабельную qDebug()? Как это, миль, конечно, пардон, называется? Может быть, можно дать гарантии относительно реентерабельности хотя бы при отсутствии пользовательского обработчика отладочных сообщений?
[...]

Вопрос реентерабельности и потокобезопасности для qDebug поднимался уже достаточно давно. qDebug может использоваться двумя путями: qDebug("bla-bla-bla") (упрощенный вариант) или qDebug() << "bla-bla-bla" (с использованием QTextStream). Сложно сказать в общем, но, как я понимаю, сама Qt внутри себя может испозовать именно первый вариант. Тогда гарантии именно реентерабельности должна давать не только Qt, но и операционная система. Например, у меня в Qt 5.3 для QFile::open вызывается qWarning, что практически тоже самое, что и qDebug:
bool QFile::open(OpenMode mode)
{
    Q_D(QFile);
    if (isOpen()) {
        qWarning("QFile::open: File (%s) already open", qPrintable(fileName()));
        return false;
    }
...
}

Дальше по коду, qWarning готовит отформатированный буфер, который потом передается в
static void qt_message_print(QtMsgType msgType, const QMessageLogContext &context, const QString &message)
{
#ifndef QT_BOOTSTRAPPED
    // qDebug, qWarning, ... macros do not check whether category is enabled
    if (!context.category || (strcmp(context.category, "default") == 0)) {
        if (QLoggingCategory *defaultCategory = QLoggingCategory::defaultCategory()) {
            if (!defaultCategory->isEnabled(msgType))
                return;
        }
    }
#endif

    if (!msgHandler)
        msgHandler = qDefaultMsgHandler;
    if (!messageHandler)
        messageHandler = qDefaultMessageHandler;

    // prevent recursion in case the message handler generates messages
    // itself, e.g. by using Qt API
    if (grabMessageHandler()) {
        // prefer new message handler over the old one
        if (msgHandler == qDefaultMsgHandler
                || messageHandler != qDefaultMessageHandler) {
            (*messageHandler)(msgType, context, message);
        } else {
            (*msgHandler)(msgType, message.toLocal8Bit().constData());
        }
        ungrabMessageHandler();
    } else {
        fprintf(stderr, "%s", message.toLocal8Bit().constData());
    }
}

Поскольку я запускался из-под оттладчика под виндами, то пошел по ветке (*messageHandler)(msgType, context, message), что в конечном счете привело к вызову OutputDebugString, поскольку отладчик перенаправил вывод под себя.

Так вот, с одной стороны, гарантий реентерабельности под виндами (во всяком случае, не для каждой версии) не дается ни для для OutputDebugString, ни для fprintf (если бы даже я пошел по той ветке). Какие же тогда могут быть претензии только к Qt? Разве что к не до конца точной документации.

С другой стороны, может, вам нужна именно потокобезопасность? Код qt_message_print не выглядит потокобезопасным, поскольку завязан на глобальные переменные, отвечающие за перенаправление отладочных потоков. Если разные потоки будут делать это перенаправление во время работы друг друга, это может плохо закончится. Но это можно обойти, если вы в своей программе единожды сделаете нужное вам перенаправление. Например, в функции main() до конструирования QApplication. Ну, или вообще ничего не будете трогать и перенаправлять. Тогда все должно работать безопасно.

Теперь об усложненном варианте qDebug() << "bla-bla-bla". Как я уже упоминал, базовая реализация qDebug для operator<< использует QTextStream. Документация дает гарантии его реентерабельности, но не дает их для потокобезопасности. Если они вам нужны, существует решение с перенаправлением вывода в нужное русло под защитой мьютекса. Например, описанное здесь. Возможно, что этого в вашем случае также окажется достаточно.
Re[3]: qDebug epic fail: "мопед не мой, я просто разместил объяву"
От: SaZ  
Дата: 10.06.14 12:12
Оценка: +1
Здравствуйте, slava_phirsov, Вы писали:

_>QT_NO_DEBUG_OUTPUT — поможет избежать нереентерабельных вызовов из реентерабельных функций-членов классов библиотеки Qt только если перекомпилировать библиотеку Qt с данным макросом (угадай, почему).


Так а что мешает перекомпилировать?
Re: qDebug epic fail: "мопед не мой, я просто разместил объяву"
От: Figaro Россия  
Дата: 09.06.14 09:09
Оценка:
Непонятно в чем проблема то? У Вас коммерческая версия Qt, тады требуйте от "масисиси" подправить документацию или саму реализацию qDebug()... Иначе не используйте...
avalon/1.0.433
Re[2]: qDebug epic fail: "мопед не мой, я просто разместил объяву"
От: slava_phirsov Россия  
Дата: 09.06.14 09:40
Оценка:
Здравствуйте, Figaro, Вы писали:

F>Непонятно в чем проблема то? У Вас коммерческая версия Qt, тады требуйте от "масисиси" подправить документацию или саму реализацию qDebug()... Иначе не используйте...


Проблема в том, что Digia избрала Microsoft-way: "мы не видим проблемы в вашей проблеме". Вполне характерная позиция для монополистов. Ну ладно, не монополистов, скорее олигополистов. "Не используйте" — классный совет. Проще всего такой совет дать и тяжелее всего ему следовать. Использовать-то что тады? Ась?
Люди! Люди, смотрите, я сошел с ума! Люди! Возлюбите друг друга! (вы чувствуете, какой бред?)
Re[2]: qDebug epic fail: "мопед не мой, я просто разместил объяву"
От: slava_phirsov Россия  
Дата: 09.06.14 15:20
Оценка:
Здравствуйте, visual_wind, Вы писали:

_>С другой стороны, может, вам нужна именно потокобезопасность? Код qt_message_print не выглядит потокобезопасным, поскольку завязан на глобальные переменные, отвечающие за перенаправление отладочных потоков. Если разные потоки будут делать это перенаправление во время работы друг друга, это может плохо закончится. Но это можно обойти, если вы в своей программе единожды сделаете нужное вам перенаправление. Например, в функции main() до конструирования QApplication. Ну, или вообще ничего не будете трогать и перенаправлять. Тогда все должно работать безопасно.


_>Теперь об усложненном варианте qDebug() << "bla-bla-bla". Как я уже упоминал, базовая реализация qDebug для operator<< использует QTextStream. Документация дает гарантии его реентерабельности, но не дает их для потокобезопасности


Каждый вызов qDebug() создает свой собственный экземпляр QDebug, а тот, в свою очередь — свой собственный экземпляр QTextStream, так что проблема потокобезопасности тут малоинтересна. Иное дело — реентерабельность.


Вообще, qt_message_print вызывает qgetenv, а она не является реентерабельной, и чем это может закончится:

//qdebug.h
inline ~QDebug() {

    if (!--stream->ref) {

        if(stream->message_output) {
            QT_TRY {
                qt_message_output(stream->type, stream->buffer.toLocal8Bit().data());
            } QT_CATCH(std::bad_alloc&) { /* We're out of memory - give up. */ }
        }

        delete stream;
    }
}

//qglobal.cpp
void qt_message_output(QtMsgType msgType, const char *buf)
{
    //....
    if (
        msgType == QtFatalMsg ||
        (
            msgType == QtWarningMsg &&
           (!qgetenv("QT_FATAL_WARNINGS").isNull())
        )
    ) {
        //....
        abort();
        //.....
    }
}

QByteArray qgetenv(const char *varName)
{
    //....
    return QByteArray(::getenv(varName));
}


The implementation of getenv is not required to be reenterant... The string pointed to ... may be statically allocated.


То есть наложившиеся вызовы getenv могут дать аварийное завершение приложения по банальному qWarning даже в случае, если переменная окружения QT_FATAL_WARNINGS не установлена.
Люди! Люди, смотрите, я сошел с ума! Люди! Возлюбите друг друга! (вы чувствуете, какой бред?)
Re[3]: qDebug epic fail: "мопед не мой, я просто разместил объяву"
От: visual_wind  
Дата: 09.06.14 15:55
Оценка:
Здравствуйте, slava_phirsov, Вы писали:

_>Каждый вызов qDebug() создает свой собственный экземпляр QDebug, а тот, в свою очередь — свой собственный экземпляр QTextStream, так что проблема потокобезопасности тут малоинтересна. Иное дело — реентерабельность.


Какждый экземпляр QTextStream пытается вывести информацию в один и тот же ресурс, не так ли? Экземпляров QTextStream много, ресурс один и тот же, мьютекса нет. Ситуация с доступом к ресурсу может как разрулиться операционной системой, так накрыться медным тазом.

_>Вообще, qt_message_print вызывает qgetenv, а она не является реентерабельной, и чем это может закончится:

[...]
_>То есть наложившиеся вызовы getenv могут дать аварийное завершение приложения по банальному qWarning даже в случае, если переменная окружения QT_FATAL_WARNINGS не установлена.

Я вижу, что вы под Линуксом, и, возможно, что для вас реентереабленость в связи с линуксовыми сигналами важнее потокобезопасности. Но по исходному коду Qt вы лишний раз подтвердили, что на реентерабельность полагаться, увы, нельзя.
Re: qDebug epic fail: "мопед не мой, я просто разместил объяву"
От: Igore Россия  
Дата: 10.06.14 07:32
Оценка:
Я так и не понял, есть проблема или нет, может тебе QT_NO_DEBUG_OUTPUT поможет.
Re[4]: qDebug epic fail: "мопед не мой, я просто разместил объяву"
От: slava_phirsov Россия  
Дата: 10.06.14 09:32
Оценка:
Здравствуйте, visual_wind, Вы писали:

_>Какждый экземпляр QTextStream пытается вывести информацию в один и тот же ресурс, не так ли? Экземпляров QTextStream много, ресурс один и тот же, мьютекса нет. Ситуация с доступом к ресурсу может как разрулиться операционной системой, так накрыться медным тазом.


Каждый экземпляр QTextStream пытается вывести информацию в свой собственный буфер:
struct Stream {
        // ...
        Stream(QtMsgType t) : ts(&buffer, QIODevice::WriteOnly) /* ... */ {}
        QTextStream ts;
        QString buffer;
        // ...

    } *stream;

inline QDebug(QtMsgType t) : stream(new Stream(t)) {}

inline QDebug &operator<<(float t) {stream->ts << t; return maybeSpace();}

inline ~QDebug() {

    if (!--stream->ref) {

        if(stream->message_output) {
            QT_TRY {
                qt_message_output(stream->type, stream->buffer.toLocal8Bit().data());
            } QT_CATCH(std::bad_alloc&) { /* We're out of memory - give up. */ }
        }

        delete stream;
    }
}


Вот qt_message_output, вызываемый под занавес — дело иное:
// Если только не Symbian, WinCE или Mac
fprintf(stderr, "%s\n", buf);
fflush(stderr);


Под *nix вызовы fprintf и fflush безопасны, но не получится ли на стандартном выходе stderr каша при одновременной записи туда из разных потоков — не знаю. Учитывая, что они не буферизуются — скорее всего, получится. Что тут под Win — тоже


_>Я вижу, что вы под Линуксом, и, возможно, что для вас реентереабленость в связи с линуксовыми сигналами важнее потокобезопасности. Но по исходному коду Qt вы лишний раз подтвердили, что на реентерабельность полагаться, увы, нельзя.


Сигналы-то здесь причем? ОК, договоримся о терминологии, благо, она изложена в документации по Qt, и изложенного там можно смело придерживаться. Реентерабельность — возможность вызова одной и той же функции из разных потоков одновременно с различными экземплярами данных. Потокобезопасность — возможность вызова одной и той же функции из разных потоков одновременно при наличии разделяемых экземпляров данных (как наиболее интересный пример — вызов функции-члена одного и того же экземпляра класса из разных потоков). Так вот, о потокобезопасности функции qDebug() говорить смысла не имеет — каждый вызов функции будет создавать собственный экземпляр класса QDebug. Имеет смысл говорить о реентерабельности этой функции (или ее отсутствии).
Люди! Люди, смотрите, я сошел с ума! Люди! Возлюбите друг друга! (вы чувствуете, какой бред?)
Re[2]: qDebug epic fail: "мопед не мой, я просто разместил объяву"
От: slava_phirsov Россия  
Дата: 10.06.14 09:41
Оценка:
Здравствуйте, Igore, Вы писали:

I>Я так и не понял, есть проблема или нет, может тебе QT_NO_DEBUG_OUTPUT поможет.


Проблема — есть.

QT_NO_DEBUG_OUTPUT — поможет избежать нереентерабельных вызовов из реентерабельных функций-членов классов библиотеки Qt только если перекомпилировать библиотеку Qt с данным макросом (угадай, почему).
Люди! Люди, смотрите, я сошел с ума! Люди! Возлюбите друг друга! (вы чувствуете, какой бред?)
Re[5]: qDebug epic fail: "мопед не мой, я просто разместил объяву"
От: visual_wind  
Дата: 10.06.14 13:43
Оценка:
Здравствуйте, slava_phirsov,

Задача, как я понял, в том, чтобы добиться реентерабельного вывода отладочных сообщений. Вы, как я вижу, смотрите на базовую реализацию для qDebug(), а я — свою ссылку из изначального поста. Мне казалось, что опасность ситуации в базовой реализации достаточно ясна. Что касается варианта, который я приводил по ссылке, то конечно же, я ошибся. Речь не о том экземпляре QTextStream, который используется внутри каждого экземпляра QDebug, а о том, который мы сами создали. И для которого из qt_message_print вместо неустраивающей нас fprintf вызывается QTextStream::operator<< , в котором и происходит вывод в ресурс (в данном примере файл), который был предварительно насетаплен в QTextStream::setDevice.

Вот простейший пример для Qt 5.3 на основе того, что было по ссылке
//main.cpp
#include "mainwindow.h"
#include <QApplication>
#include <QtDebug>
#include <iostream>
#include <QFile>

QTextStream g_nonstderr;
QFile g_debugLogFile;
QMutex g_mutex;

void debugMessageHandler(QtMsgType type, const QMessageLogContext& context, const QString& msg)
{
    g_mutex.lock();
    switch ( type )
    {
        case QtDebugMsg:
            g_nonstderr << QString( msg ) << endl;
            break;
        case QtWarningMsg:
            g_nonstderr << "Warning:" << msg << endl;
            break;
        case QtCriticalMsg:
            g_nonstderr << "Critical:" << msg << endl;
            break;
        case QtFatalMsg:
            g_nonstderr << "Fatal:" << msg << endl;
            abort();
    }
    g_mutex.unlock();
}

void setupDebugRedirection()
{
    g_debugLogFile.setFileName("debugOut.txt");
    g_nonstderr.setDevice( &g_debugLogFile );
    g_debugLogFile.open( QIODevice::WriteOnly | QIODevice::Text );
    qInstallMessageHandler( debugMessageHandler );
}

int main(int argc, char *argv[])
{
    setupDebugRedirection();
    qWarning() << "warning-bla-bla-bla";

    QApplication a(argc, argv);
    MainWindow w;
    w.show();

    return a.exec();
}


Если вас не устраивает этот вариант, и вы хотите остаться в базовой реализации qDebug, то, в Qt 5.3 присутствует QLoggingCategory. Здесь показано, как с ее помощью глобально задизейблить отладочные сообщения (чтобы библиотека ничего не выводила), а для себя использовать отдельный фильтр.
 
Подождите ...
Wait...
Пока на собственное сообщение не было ответов, его можно удалить.