Wywołanie destruktora klasy w trakcie zapytania do bazy danych

0

Cześć, na warsztat bierzemy prosty komunikator, który działa w oparciu o bazę danych.
W drugim wątku program odpytuje bazę danych w pętli. Jeśli uda mu się wykonać pętlę bez przerwania wszystko gra.
Jeśli w trakcie wykonania pętli próbuję uruchomić destruktor tej klasy, który usuwa połączenie z bazą - program się wysypuje.
Poniżej przykładowa pętla, która jest uruchamiana gdy startuje drugi wątek:

for (int i = 0; i < 1000; i++)
    {
        if (query.exec(messageIdQuery))
        {
            if (query.size() > 0)
            {
                query.last();
                qDebug() << query.value("message_id").toUInt();
                if (query.value("message_id").toUInt() > lastReadMessageId)
                {
                    emit wroteNewMessageInDatabase();
                }
            }
        }
    }
}

W głównym wątku, bo zamknięciu jednego z okien, chcę uruchomić destruktor dla powyższej klasy, który wygląda tak:

MessagesController::~MessagesController()
{
    database = QSqlDatabase();
    QSqlDatabase::removeDatabase(QString("messagesControllerThreadUser%1").arg(converserId));
}

Co może być przyczyną crash'a?

4

UB, nie możesz robić takich rzecz podczas działania innego wątku. Ba, gdyby poczytać dokumentację Qt, to można znaleźć nawet takie rzeczy:

A connection can only be used from within the thread that created it. Moving connections between threads or creating queries from a different thread is not supported.

Inaczej mówiąc: niech wątek odpowiedzialny za komunikację z DB sam sobie tworzy połączenie i tylko on z niego korzysta. A zabijanie wątków inaczej niż "gracefully" powinno wiązać się z ponownym uruchomieniem aplikacji.

0
kq napisał(a):

UB, nie możesz robić takich rzecz podczas działania innego wątku. Ba, gdyby poczytać dokumentację Qt, to można znaleźć nawet takie rzeczy:

A connection can only be used from within the thread that created it. Moving connections between threads or creating queries from a different thread is not supported.

Inaczej mówiąc: niech wątek odpowiedzialny za komunikację z DB sam sobie tworzy połączenie i tylko on z niego korzysta. A zabijanie wątków inaczej niż "gracefully" powinno wiązać się z ponownym uruchomieniem aplikacji.

Wiesz co, wydaje mi się, że właśnie tak robię. Ten drugi wątek tworzy nowe połączenie z bazą i destruktor tej klasy z drugiego wątku to połączenie usuwa. Jedynie destruktor wywoływany jest z innego wątku, ale to chyba nie jest problemem. Problem występuje, kiedy odpytuję bazę i w trakcie tego odpytywania wywołam destruktor. Jeśli pętla skończy odpytywać bazę to wtedy destruktor działa prawidłowo. Może brakuje czegoś w tym destruktorze?

1
haracz napisał(a):

(...) destruktor tej klasy z drugiego wątku to połączenie usuwa. Jedynie destruktor wywoływany jest z innego wątku (...)

Widzisz sprzeczność?
Zasadniczo powinieneś, w przypadku chęci zakończenia połączenia, w jakiś sposób poinformować wątek-właściciela tego połączenia i to on powinien się tym zająć. Nawiasem mówiąc, cytat, który przytoczył @kq mówi jasno, że nawet używanie połączenia w innych wątkach jest niewspierane.

0

Ten drugi wątek tworzy nowe połączenie z bazą i destruktor tej klasy z drugiego wątku to połączenie usuwa.

Sam osobny obiekt nie wystarczy, tutaj w skrócie napisałem co powinno się zrobić https://4programmers.net/Forum/C_i_C++/366522-zapytanie_do_bazy_danych_z_dwoch_roznych_watkow?p=1893716#id1893716

(edit)
Cytat z tamtego wątku

Wg. dokumentacji, żeby mieć dwa różne połączenia, najpierw dodajesz sobie nazwę poprzez addDatabase którego potem używasz w zawołaniu database. Dla każdegu wątku musisz użyć dwóch różnych nazw połączeń.

3

Wątki to złoto głupców.
To jest taka błyskotka, o której wszyscy gadają i chcą używać, a prawda jest taka, że ich użyteczność jest ograniczona, a uzyskanie porwanej synchronizacji umyka większości deweloperów.
Gorąco polecam unikanie wątków i nie pchanie ich na siłę.
Qt ma dość miłę w użyci asynchroniczne API, które spokojnie załatwi taki prosty przypadek.

Co do samego tematu, załączony kod jest niekompletny. Tekst wspomina, coś o wątkach, ale ani jedna linka tego kodu na to nie wskazuje,
Mało tego, tekst mówi o problemach z zarządzanie czasem życia obiektu, a jedyny ślad kody z tym związany to definicja jakiegoś destruktora klasy, która nie wiadomo jak jest połączona z resztą kodu,

1
haracz napisał(a):

właśnie tak robię.

No właśnie nie:

haracz napisał(a):

destruktor wywoływany jest z innego wątku

haracz napisał(a):

Problem występuje, kiedy odpytuję bazę i w trakcie tego odpytywania wywołam destruktor.

🤦‍♂️ Czyli problem występuje gdy 2 różne wątki działają bez synchronizacji na tych samych danych...

Masz tutaj eksperyment myślowy, zastanów się dlaczego program się wysypuje:

int main()
{
    auto ptr = make_unique<std::map<int,int>>();
    std::map<int, int>& m = *ptr;
    for (int i = 0; i < 10; ++i)
        m[i] = i;

    thread t{[&m] {
        while (true) {
            for (auto const& [k, v] : m) {
                if (v > 10)
                    std::cout << k << " " << v << std::endl;
                std::this_thread::sleep_for(10ms);
            }
        }
    }};

    std::this_thread::sleep_for(100ms);
    m[1] = 11;
    std::this_thread::sleep_for(100ms);
    m[1] = 1;

    ptr.reset();

    t.join();
}

https://wandbox.org/permlink/QMJfgAN9g1YLYQ0d

0
MarekR22 napisał(a):

Co do samego tematu, załączony kod jest niekompletny. Tekst wspomina, coś o wątkach, ale ani jedna linka tego kodu na to nie wskazuje,
Mało tego, tekst mówi o problemach z zarządzanie czasem życia obiektu, a jedyny ślad kody z tym związany to definicja jakiegoś destruktora klasy, która nie wiadomo jak jest połączona z resztą kodu,

Sorka za skromy fragment kodu, nie chciałem zaśmiecać, to wygląda tak:

// OKNO CZATU
void ChatWindow::makeThread()
{
    messagesController = new MessagesController(converserId, lastReadMessageId);
    QThread *thread = new QThread;
    messagesController->moveToThread(thread);
    connect(thread, &QThread::started, messagesController, &MessagesController::run);
    connect(messagesController, &MessagesController::wroteNewMessageInDatabase, this, &ChatWindow::readNewMessages);
    thread->start();
}

// desktruktor tego okna jest wywoływany po naciśnięciu na 'X' dzięki poniższej metodzie
void closeEvent(QCloseEvent *event) { this->deleteLater(); }

// destruktor wygląda tak, po zamknięciu okna czatu chcę usunąć obiekt skojarzony z drugim wątkiem
ChatWindow::~ChatWindow()
{
    messagesController->~MessagesController();
    delete ui;
    MainWindow::activeWindowsList.removeOne(converserId);
}

// KLASA KONTROLERA SPRAWDZAJĄCEGO CZY SĄ NOWE WIADOMOŚCI

#include <QDebug>
#include <QSqlQuery>
#include <QString>
#include "messagescontroller.h"
#include "loginpage.h"

MessagesController::MessagesController(quint32 converserId, quint32 lastReadMessageId, QObject *parent)
    : QObject{parent},
      converserId{converserId},
      lastReadMessageId{lastReadMessageId}
{
    database = QSqlDatabase::addDatabase("QMYSQL", QString("messagesControllerThreadUser%1").arg(converserId));
}

MessagesController::~MessagesController()
{
    database = QSqlDatabase();
    QSqlDatabase::removeDatabase(QString("messagesControllerThreadUser%1").arg(converserId));
    qDebug() << "Removed " + QString("messagesControllerThreadUser%1").arg(converserId);
}

void MessagesController::run()
{
    qDebug() << QString("messagesControllerThreadUser%1").arg(converserId);
    checkNewMessages();
}

void MessagesController::checkNewMessages()
{
    QString messageIdQuery = "SELECT message_id FROM " + QString::number(LoginPage::getUserId()) + "_chat_" + QString::number(converserId);
    LoginPage::connectToDatabase(database);
    QSqlQuery query(database);

    //while(true)
    for (int i = 0; i < 1000; i++)
    {
        if (query.exec(messageIdQuery))
        {
            if (query.size() > 0)
            {
                query.last();
                qDebug() << query.value("message_id").toUInt();
                if (query.value("message_id").toUInt() > lastReadMessageId)
                {
                    emit wroteNewMessageInDatabase();
                }
            }
        }
    }
}

Czyli z grubsza... Gdy otwierane jest okno czatu z konkretnym użytkownikiem, tworzony ma być nowy wątek sprawdzający nowe wiadomości, a w chwili zamknięcia okna czatu wątek i połączenie z bazą powinny być usuwane.

0

Wystarczyło dodać publiczną flagę, która ustawiała się na true kiedy okno czatu się zamykało, cała filozofia.

//...
public:
  bool isChatWindowClosed;
//...

// Pętla odpytująca bazę

while(true)
{
    if (this->isChatWindowClosed)
    {
        this->~MessagesController();
        break;
    }

    if (query.exec(messageIdQuery))
    {
        if (query.size() > 0)
        {
            query.last();
            qDebug() << query.value("message_id").toUInt();
            if (query.value("message_id").toUInt() > lastReadMessageId)
            {
                emit wroteNewMessageInDatabase();
            }
        }
    }
}

// Metoda uruchamiana przy zamykaniu okna czatu
void ChatWindow::closeEvent(QCloseEvent *event)
{
    messagesController->isChatWindowClosed = true;
    this->deleteLater();
}

1 użytkowników online, w tym zalogowanych: 0, gości: 1