Wywołanie funkcji innej klasy z oddzielnego wątku.

0

Witam,

Mam problem dotyczący wywoływania funkcji klasy MainWindow z oddzielnego wątku.
Funkcja której chcę użyć appendMessage() nie jest dostępna w wątku MainThread() (jest niezadeklarowana).

Chciałbym dowiedzieć się jakim sposobem można użyć funkcji appendMessage() w innych wątkach.

Dołączam fragment kodu:

DWORD WINAPI MainThread(LPVOID lpParam)
{
    while(!abortth) {
        if (!pauseth) {
            nlohmann::json data = mprog.readjsondata();
            if (data["command"] == "ping") {
                mprog.sendText("{\"command\":\"pong\"}");
            }
            if (data["command"] == "newGlobalChatMessage") {
                appendMessage(QString::fromStdString(data["author"].dump()),QString::fromStdString(data["value"].dump()));
            }
        }
    }
    return 0;
}


MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent)
    , ui(new Ui::MainWindow)
{
    ui->setupUi(this);
    ui->textEdit->setFocusPolicy(Qt::NoFocus);
    ui->textEdit->setReadOnly(true);
    tableFormat.setBorder(0);
    mainth = CreateThread(NULL, 0, MainThread, NULL, 0, NULL);
}

void MainWindow::appendMessage(const QString &from, const QString &message)
{
    if (from.isEmpty() || message.isEmpty())
        return;

    QTextCursor cursor(ui->textEdit->textCursor());
    cursor.movePosition(QTextCursor::End);
    QTextTable *table = cursor.insertTable(1, 2, tableFormat);
    table->cellAt(0, 0).firstCursorPosition().insertText('<' + from + "> ");
    table->cellAt(0, 1).firstCursorPosition().insertText(message);
    QScrollBar *bar = ui->textEdit->verticalScrollBar();
    bar->setValue(bar->maximum());
}

Prosiłbym o pomoc.

Pozdrawiam

3

Krótko: nie można i nie będzie można. Jak dla mnie problem XY i widzę parę sporych kłopotów z tym kodem:
a. nieznajomość C++ (appendMessage to jest funkcja klasy, musisz obiekt typu MainWindow przekazać jakoś do tego MainThread)
b. nieznajomość QT - nie można używać obiektów GUI w innym wątku niż główny, narzucony przez framework
c. mieszanie API. QT ma swoje API do wątków. Przy czym patrz pkt nr 2. Ale nawet jeśli 2 by nie obowiązywało to QT ma swoje API do wątków i mieszanie systemowego i frameworkowego to proszenie się o kłopoty, zwłaszcza kiedy FW używa jeszcze własnej pętli zdarzeń. No nie, ew.: jeśli faktycznie potrzebowałbyś to robić to napisałbyś odpowiedni przykład a nie pytał tu o podstawy. Life is brutal, sorry ;)

Długo: powinieneś całą tę funkcję MainThread przepisać do API kompatybilnego z QThread a następnie
a. odpowiednio połączyć sygnały i sloty z (bodajże QQueuedConnection potrafi prawidłowo przekazywać wiadomości między wątkami)
b. przesunąć Twój reader, worker, cokolwiek to jest do tego QThreadu (metoda moveToThread)
c. wystartować thread.
d. ...profit!

Możesz też dziedziczyć z QThread, ale to niesie ze sobą kilka problemów, zobacz tu.

Do poczytania:
QThread
QStateMachine - jak działasz wielowątkowo to się bardzo przydaje do zarządzania stanem workera. Niekoniecznie dla prostych operacji ma to sens, ale warto znać.

2

Funkcja MainThread nie jest funkcją składową (metodą) klasy MainWindow. Tym samym nie ma dostępnego this w tej funkcji (niejawnie również).
Możesz przekazać this jako parametr, który system wołając MainThread przekaże jako pierwszy argument.

Z MSDN:

HANDLE CreateThread(
  [in, optional]  LPSECURITY_ATTRIBUTES   lpThreadAttributes,
  [in]            SIZE_T                  dwStackSize,
  [in]            LPTHREAD_START_ROUTINE  lpStartAddress,
  [in, optional]  __drv_aliasesMem LPVOID lpParameter,
  [in]            DWORD                   dwCreationFlags,
  [out, optional] LPDWORD                 lpThreadId
);
[in, optional] lpParameter

A pointer to a variable to be passed to the thread.

Więc robisz tak:

mainth = CreateThread(NULL, 0, MainThread, NULL, 0, NULL);

zmieniasz na:

mainth = CreateThread(NULL, 0, MainThread, this, 0, NULL);

Wtedy w tej funkcji:

DWORD WINAPI MainThread(LPVOID lpParam)

lpParam jest wskaźnikiem na MainWindow. Musisz zadbać aby obiekt przekazany do wątku był dostępny przez cały czas życia wątku (albo przynajmniej w momencie kiedy operujemy na obiekcie). Całkiem łatwo możesz się upewnić, że obiekt ciągle istnieje bo to MainWindow zarządza wątkiem. Więc wywołanie ExitThread w destruktorze MainWindow powinno zrobić robotę (+ może WaitForSingleObject).

Teraz pozostaje jedynie zrzutować wskaźnik, i masz dostęp do metod klasy:

DWORD WINAPI MainThread(LPVOID lpParam)
{
    MainWindow *const mainWindow = static_cast<MainWindow *>(lpParam);
    while(!abortth) {
        if (!pauseth) {
            nlohmann::json data = mprog.readjsondata();
            if (data["command"] == "ping") {
                mprog.sendText("{\"command\":\"pong\"}");
            }
            if (data["command"] == "newGlobalChatMessage") {
                mainWindow->appendMessage(QString::fromStdString(data["author"].dump()),QString::fromStdString(data["value"].dump()));
            }
        }
    }
    return 0;
}

Problemem będzie to, że twój program nie będzie działał dobrze. Metoda appendMessage odwołuje się do pamięci, która jest również używana w głównym wątku aplikacji. Przez to wysoce prawdopodobne jest to, że twój program będzie się sypał. Równoległe pisanie do pamięci i odczyt z pamięci (tej samej) z dwóch różnych wątków spowoduje crash całego programu.

Na przykład tutaj:

    QScrollBar *bar = ui->textEdit->verticalScrollBar();
    bar->setValue(bar->maximum());

jest modyfikowana pamięć - wartość w klasie QScrollBar. Wysoce prawdopodobne jest, że główny wątek rysujący okno aplikacji będzie czytało tą samą wartość z tego samego obiektu UI. QScrollBar::setValue(...) i QScrollBar::value() nie mają mechanizmu synchronizacji między wątkami (to wołanie nie jest thread-safe).

Żeby to naprawić musisz jakoś zsynchronizować między sobą dwa wątki. Tutaj myślę najlepszym i najprostszym rozwiązaniem pewnie będzie kolejka wiadomości.
Tutaj jest na pewno kilka możliwości: bazujące na tym co już jest w Qt albo bazujące na własnym kodzie.

Bazując na Qt:

Można stworzyć własną klasę eventu, którą wyślemy do MainWindow i obsłużymy tam:

class MyMessageEvent : public QEvent
{
public:
    static const Type s_type = static_cast< Type >( QEvent::User + 1 );

    explicit MyMessageEvent( QString from, QString message ) : QEvent( s_type ), m_from( from ), m_message( message ) {}

    const QString &GetFrom() const { returm m_from; }
    const QString &GetMessage() const { return m_message; }

private:
    QString m_from;
    QString m_message;
};

// gdzieś w .cpp
const QEvent::Type MyMessageEvent::s_type;

Czytając z:
https://doc.qt.io/qt-5/threads-qobject.html

You can manually post events to any object in any thread at any time using the thread-safe function QCoreApplication::postEvent(). The events will automatically be dispatched by the event loop of the thread where the object was created.

Więc to:

            if (data["command"] == "newGlobalChatMessage") {
                appendMessage(QString::fromStdString(data["author"].dump()),QString::fromStdString(data["value"].dump()));
            }

zamieniamy na:

            if (data["command"] == "newGlobalChatMessage") {
                QApplication::postEvent(mainWindow, new MyMessageEvent(QString::fromStdString(data["author"].dump()),QString::fromStdString(data["value"].dump())));
            }

A w MainWindow musimy obsłużyć event:

class MainWindow /* .... coś tutaj jest */
{
 /* .... coś tutaj jest */
    virtual bool event( QEvent *event ) override
 /* .... coś tutaj jest */
};
bool MainWindow::event( QEvent *event )
{
    if( event->type() == MyMessageEvent::s_type )
    {
        MyMessageEvent *const typedEvent = static_cast< MyMessageEvent * >( event );
        appendMessage( typedEvent->GetFrom(), typedEvent->GetMessage() );
        return true;
    }
    else return <<<klasa bazowa>>>::event( event );
}

Druga opcja to do synchronizacji wątków możesz użyć klasy z Qt: QReadWriteLock (ewentualnie QMutex, albo z STL: std::mutex).

Jednocześnie w głównym wątku aplikacji musisz jakoś skonsumować kolejkę wiadomości i dodać ją do UI.
Nie będę pokazywał, poszukać musisz już sam.

Dużym problemem również jest to, że korzystasz z frameworku, który oferuje już wielowątkowość i mieszasz do tego API systemowe. Chcąc przeportować swoją aplikację na np. Linux, będziesz musiał napisać oddzielną implementację na Linuxa. Poza tym, korzystając z API do multithreadingu z Qt pewnie będziesz miał dostępne od ręki prostsze mechanizmy do synchronizacji.

1

qt_thread_demo.zipNo to coby nie zostać gołosłownym, przykład:

Worker: czyli to co u Ciebie powinno czytać; zrobione asynchronicznie, wywoływane co 3 sek.

#ifndef WORKER_HPP
#define WORKER_HPP

#include <QObject>
#include <QString>

class QTimer;

class Worker : public QObject
{
    Q_OBJECT
public:
    Worker(QObject* parent=nullptr);

    void doWork(); //odczyt
    void finish();
    void start();
signals:
    void postData(QString, int); //sygnal do podawania danych dalej
private:
    QTimer* timer;
    int x;
};

#endif // WORKER_HPP

#include "worker.hpp"
#include <QTimer>
#include <QDebug>
#include <QThread>

Worker::Worker(QObject* parent)
    : QObject{parent}
    , timer{new QTimer{this}} //wazne zeby timer mial parenta, wtedy zostanie przesunięty do odpowiedniego wątku przez moveToThread
    , x{0}
    {
        timer->setInterval(3000);
        timer->setSingleShot(false);
        timer->callOnTimeout(this, &Worker::doWork);
    }

void Worker::doWork()
{
    if (x>=30) {
        finish();
    } else {
        ++x; //tu byłby odczyt tego jsona  
        emit postData(x%2 ? QStringLiteral("abc"):QStringLiteral("def"), x); //tutaj podanie go dalej
    }
}

void Worker::finish()
{
    timer->stop();
    qDebug() << QThread::currentThreadId() << " stop";
}

void Worker::start()
{
    qDebug() << QThread::currentThreadId() << " start";
    if (!timer->isActive()) {
        timer->start();
    }
}

Główny program. MainWindow trzymające workera to niekoniecznie najlepszy design (może powinno tylko referencję), natomiast co jest ważne to co się dzieje z QThread i Workerem:

#ifndef MAINWINDOW_H
#define MAINWINDOW_H

#include <QMainWindow>
#include "worker.hpp"
#include <QThread>

QT_BEGIN_NAMESPACE
namespace Ui { class MainWindow; }
QT_END_NAMESPACE

class MainWindow : public QMainWindow
{
    Q_OBJECT
public:
    MainWindow(QWidget *parent = nullptr);
    ~MainWindow();

    void updateData(const QString& s, int x); //popatrz na sygnaturę postData z workera!


private:
    Ui::MainWindow *ui;
    //dwa poniżej możnaby z powodzeniem zamknąć w klasie ThreadedWorker albo podobnej
    Worker worker;
    QThread workerThread; //wątek
};
#endif // MAINWINDOW_H
#include "mainwindow.h"
#include "./ui_mainwindow.h"
#include <QDebug>
#include <QThread>


MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent)
    , ui(new Ui::MainWindow)
    , worker{}
    , workerThread{}
{
    ui->setupUi(this);
    worker.moveToThread(&workerThread); //worker będzie pracować w innym wątku
    connect(&worker, &Worker::postData, this, &MainWindow::updateData);  
   //postData workers przekazuje dane wprost do UpdateData. Co ważne, to jest thread-safe!
    connect(
        ui->pushButton,
        &QPushButton::clicked,
        &worker,
        [&wrk=worker](bool){wrk.start();});
    //start workera z klawisza nr 1
    connect(
        ui->pushButton_2,
        &QPushButton::clicked,
        &worker,
        [&wrk=worker](bool){wrk.finish();});
    //analogicznie stop z drugiego
    workerThread.start(); //start nowego wątku. Póki co nic się w nim nie dzieje!
    qDebug() <<  QThread::currentThreadId() << " App start";
}

MainWindow::~MainWindow()
{
    worker.finish();
    workerThread.quit();
    workerThread.wait();
//tu znowu lepszy by był chyba ThreadedWorker z jednym destruktorem.... 
    delete ui;
    qDebug() <<  QThread::currentThreadId() << " App exit";
}

void MainWindow::updateData(const QString& s, const int x)
{
    //no prosta rzecz: wypisz cokolwiek zostanie podane 
    ui->lineEdit->setText(s);
    ui->lineEdit_2->setText(QString::number(x));
}

Alternatywnie możesz podpiąć sygnał QThread::started do doWork (z jakąś pętlą w środku), żeby worker startował od razu przy zawołaniu startu wątku ale wtedy funkcja zajmie cały czas wątku i nie będzie można skorzystać z dobrodziejstw sygnałów i slotów.

Jedna uwaga: jeśli robisz connect po moveToThread a przed startem to typ połączenia będzie defaultowo Qt::QueuedConnection, czyli thread-safe, wykonywany w wątku odbiorcy.
Jeśli byś wykonał najpierw connect a potem moveToThread, skończy się to Qt::DirectConnection czyli wykonywanie w wątku wysyłającego sygnał, czego raczej nie chcemy. Dla pewności może typ połączenia podać explicite:

    connect(
        ui->pushButton,
        &QPushButton::clicked,
        &worker,
        [&wrk=worker](bool){wrk.start();},
        Qt::QueuedConnection);

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