Funkcja w wątku nie wywołuje się dwa razy przez QMetaObject::invokeMethod() w połączeniu z QThread::quit()

0

Cześć,
Utworzyłem aplikację konsolową, która w pliku main.cpp ma następujący kod:

#include <QCoreApplication>
#include <QThread>
#include <QObject>

class file_logger : public QObject
{
  Q_OBJECT
public slots:
  void save_log()
  {
    printf("thread before sleep\n");
    QThread::sleep(3);
    printf("thread after sleep\n");
    printf("save_log\n");
  }
};

class io_thread : public QThread
{
  Q_OBJECT
public:
  ~io_thread()
  {
    printf("thread before quit\n");
    quit();
    printf("thread after quit\n");
    printf("thread before wait\n");
    wait();
    printf("thread after wait\n");
  }

  file_logger * file_log;

signals:
  void init_finished();

protected:
  void run()
  {
    file_log = new file_logger();

    emit init_finished();
    exec();
    //QCoreApplication::processEvents();

    delete file_log;
  }
};

int main(int argc, char *argv[])
{
  QCoreApplication a(argc, argv);

  io_thread thread;

  QEventLoop loop;
  QObject::connect(&thread, &io_thread::init_finished, &loop, &QEventLoop::quit, Qt::QueuedConnection);

  thread.start();
  loop.exec();

  printf("invoke 1\n");
  QMetaObject::invokeMethod(thread.file_log, "save_log", Qt::QueuedConnection);
  printf("invoke 2\n");
  QMetaObject::invokeMethod(thread.file_log, "save_log", Qt::QueuedConnection);

  return 0;
}

#include "main.moc"
  1. Dlaczego funkcja file_logger::save_log() nie wywołuje się dwa razy tylko jeden raz, pomimo tego, że są dwa wywołania invokeMethod?
  2. Czy wywołanie QThread::quit() nie powinno dodatkowo przetworzyć zakolejkowanych wywołań?
  3. Jeśli odkomentuję linijkę:
    //QCoreApplication::processEvents();

to wtedy jest ok, są dwa wywołania save_log(). Czy to jest poprawne użycie?
4. Jeśli zamiast reimplementacji funkcji QThread::run() użyłbym QObject::moveToThread() dla obiektu klasy file_logger to wywołanie save_log() także nastąpiłoby tylko raz. Jak wtedy poradzić sobie z problemem bez reimplementacji run()?

2

Bo program się kończy zanim wątek zdąży przetworzyć skolejkowanie save_log.
Strasznie zagmatwałeś funkcję main i użycie event loop-a, na dodatek kończysz even loop zaraz po inicjalizacji wątku.
Powinno być tak:

int main(int argc, char *argv[])
{
  QCoreApplication a(argc, argv);

  io_thread thread;
  thread.start();
 
  printf("invoke 1\n");
  QMetaObject::invokeMethod(thread.file_log, "save_log", Qt::QueuedConnection);
  printf("invoke 2\n");
  QMetaObject::invokeMethod(thread.file_log, "save_log", Qt::QueuedConnection);

  QTimer::singleShot(700, &a, SLOT(quit())); // potrzebny jest jakiś sygnał kończący even loopa
  a.exec();

  return 0;
}

Jak będę miał jeszcze chwilę, to może bardziej poprawię jeszcze ten kod.

0
MarekR22 napisał(a):

Powinno być tak:

int main(int argc, char *argv[])
{
  QCoreApplication a(argc, argv);

  io_thread thread;
  thread.start();
 
  printf("invoke 1\n");
  QMetaObject::invokeMethod(thread.file_log, "save_log", Qt::QueuedConnection);
  printf("invoke 2\n");
  QMetaObject::invokeMethod(thread.file_log, "save_log", Qt::QueuedConnection);

  QTimer::singleShot(700, &a, SLOT(quit())); // potrzebny jest jakiś sygnał kończący even loopa
  a.exec();

  return 0;
}

Jak będę miał jeszcze chwilę, to może bardziej poprawię jeszcze ten kod.

Niestety uruchomienie Twojego kodu w main zamiast z pierwszego posta powoduje crash aplikacji, bo wątek próbuje dobrać się do wskaźnika file_log, który nie jest w danym momencie zainicjalizowany.

Może na potrzeby przykładu oznaczę:
0 - to wątek główny, tam gdzie wykonuje się funkcja main,
1 - to wątek dodatkowy (io_thread), tam gdzie wykonuje się funkcja QThread::run().

Co właściwie chcę osiągnąć:

  • wywołania save_log powinny wykonać się dwa razy, pomimo tego, że program się kończy. Czyli jeśli wątek 0 nie ma już co robić i będzie się kończył to musi poczekać, aż wykonają się 2x save_log w wątku 1,
  • kod taki powinien działać z jak i bez wywoływania na końcu funkcji main event loopa dla obiektu a typu QCoreApplication. Czyli jakbym wywołał save_log przez invokeMethod po a.exec() to musi się wywołać,
MarekR22 napisał(a):

Strasznie zagmatwałeś funkcję main i użycie event loop-a, na dodatek kończysz even loop zaraz po inicjalizacji wątku.

Ten event loop nie ma służyć jako główna pętla dla aplikacji. To tylko synchronizacja dla wątków. Wątek 0 ma zaczekać (w loop.exec();) aż wątek 1 zwróci sygnał init_finished, w ten sposób mam pewność, że wykonała się linia:
file_log = new file_logger(); oraz uruchomiła się pętla zdarzeń dla wątku 1.

MarekR22 napisał(a):

Bo program się kończy zanim wątek zdąży przetworzyć skolejkowanie save_log.

No właśnie program się kończy, ale przez to następuje wywołanie destruktora io_thread, w którym są wywołania:
quit(); wait();
wait() - oznacza, że wątek 0 ma zaczekać aż QThread::run() się zakończy.

Natomiast quit() przerywa pętlę zdarzeń (exec()) w QThread::run(). Jednak w dokumentacji Qt nie ma nic o tym, że czekające, zakolejkowane zdarzenia będą przetworzone w momencie wywołania quit(). I chyba tak jest.
Dlatego wywołanie QCoreApplication::processEvents(); po exec() w QThread::run() rozwiązuje problem. Aczkolwiek liczyłem, że dowiem się czy dobrze rozumuję. :)

0
int main(int argc, char *argv[])
{
  QCoreApplication a(argc, argv);
 
  QThread thread;
  file_logger *logger = new file_logger();
  logger->moveToThread(&thread);
  thread.start();

  printf("invoke 1\n");
  QMetaObject::invokeMethod(logger, "save_log", Qt::QueuedConnection);

  printf("invoke 2\n");
  QMetaObject::invokeMethod(logger, "save_log", Qt::QueuedConnection);

  logger->deleteLater();

  QTimer::singleShot(700, &a, SLOT(quit())); // potrzebny jest jakiś sygnał kończący even loopa
  return a.exec();
}

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