QTextStream – szybki dostęp wielowątkowy i niestabilność



Generalnie ciężko nazwać mi to problemem z wielowątkowością, ponieważ zależy to od wewnętrznej implementacji Qt, to jednak okazuję się, że używanie QTextStream jest nawet niebezpieczne – nie o tyle, że dane zapisane za jego pomocą będą bezużyteczne, ale potrafi on wyłączyć całą aplikację w trybie awaryjnym! Takie rzeczy działy mi się na Qt 5.9.8 MSVC 2017 x64 wraz z Visual Studio 16.3.4.

Kod źródłowy i środowisko

Poniższa funkcja jest używana jako uchwyt dla wiadomości z systemu debugowania Qt. Także została ona załadowana pośrednio za pomocą qInstallMessageHandler(). Kod źródłowy w uproszczonej formie wygląda tak:

class MyLogger {
public:
  // Konstruktor 
  QFile* _file;
  QString _fileName = "filename.log";
  QFileStream* _fileStream;
  MyLogger() {
    _file = new QFile(_fileName);
    bool isOpened = _file->open(QIODevice::Append | QIODevice::Text | QIODevice::WriteOnly | QIODevice::Unbuffered);
    if (!isOpened) {
      // koniec z aplikacja
    } else {
      _fileStream = new QTextStream(_file);
    }        
  }

  // Uchwyt zainstalowany przez qInstallMessageHandler
  MessageHandler(QtMsgType type, const QMessageLogContext& context, const QString& message)
  {
    QString myMessage = message;
    // Przygotowanie wiadomości
    *_fileStream << messageLine;
  }
};
W takim przypadku, kiedy wiadomości zrobi się odrobinę więcej może natknąć się na dziwne błędy na  temat uszkodzenia stosu takie jak "Invalid address specified to RtlValidateHeap"!

Gdzie leży problem i rozwiązanie

Ciężko jest mi określić gdzie problem dokładnie leży, ponieważ siedzi to gdzieś w bibliotekach Qt. Jeśli ktoś ma jakąś większą wiedzę na ten temat to z chęcią się dowiem. 
Najszybsze i najprostsze, acz nienajlepsze, rozwiazanie, to zapis bezpośrednio do pliku, czyli MessageHandler powinien wyglądać tak:  

  // Uchwyt zainstalowany przez qInstallMessageHandler
  MessageHandler(QtMsgType type, const QMessageLogContext& context, const QString& message)
  {
    QString myMessage = message;
    // Przygotowanie wiadomości
    _file->write(myMessage.toStdString().c_str(), myMessage.toStdString().length());
  }
};
Jednak z tak prostym rozwiązaniem należy uważać! Otóż może dojść do tego, że dwa szybko następujące po sobie wywołania MessageHandler mogą spowodować że dwie różne wiadomości wymieszają się w pliku i będą nie do odczytu!.

Aktualizacja 2019-11-04

Okazało się, że zunięcie flagi QIODevice::Text podczas otwierania pliku zwiększa odrobine stabilność QTexyStream – czasem można było zaobserwować w _fileStream, że ustawił sobie on flagę oznaczającą błąd zapisu (dostępna za pomocą QTextStream::status()) i potem bezpiecznie ignorowane były wszystkie inne polecenia pisania do tego strumienia – takie zachowanie według mnie powinno być zawsze niezależnie od flagi czy innych zmiennych – wystąpił błąd - odłączamy się od działania i pa pa, w szczególności kiedy kod jest tworzony bezwyjątkowo. 
Co ciekawsze, flaga QIODevice::Text wprowadzała kolejny problem: otóż co jakiś czas w losowych momentach dodawana była pusta linia. Także jeśli borykasz się z problemem magicznych pustych linii przy zapisie do pliku, usuń tę flagę. 

Komentarze