本文へスキップします。

本文へ

【全Qt】
【全・Qt】SRAロゴ
H1

技術記事:QThread の started() と finished() シグナルの送信についての考察

技術記事

QThread の started() と finished() シグナルの送信についての考察

(掲載 2025年3月29日)

デバッグ

まえがき

QThread の started() シグナルをスロットに接続したり、QThread の finished() シグナルを deleteLater() スロットに接続する際、これらのシグナルがどのスレッドで呼び出され、スロットがどの接続方式で実行されるかを把握して使用されているでしょうか。

例えば、メインスレッドと Worker スレッド間でシグナルとスロットがどのように連携するかを理解しないと、意図しないタイミングに処理が実行さる可能性があります。

特に注意すべき点として、QThread の started() シグナルはサブスレッドから発信されますが、接続されているスロットがメインスレッドで実行される場合、シグナルとスロットの間にイベントキューを介するため、実行に遅延が生じる可能性があります。また、QThread の finished() シグナルに対して deleteLater() を呼び出す場合も、オブジェクトの削除が遅延するだけでなく、想定外のスレッドで処理が行われる可能性に対して注意が必要です。

こうした問題を回避するためには、各シグナルやスロットの呼び出し元と実行先のスレッドコンテキストを明確に把握し、どの接続方式が使われるかを理解することが重要です。これにより、スレッド間の通信が正確に行われ、プログラム全体の安定性向上につながります。

QThread の started() と QThread の finished() シグナルの送信タイミング

QThread の start() を呼んでから run() が実行される流れは、以下のようになっています。

  1. OS のスレッドの開始
  2. QThread の started() シグナルの送信
  3. QThread の run() の実行の開始
  4. イベントループの実行開始と終了 (デフォルト)
  5. QThread の run() の実行の終了
  6. QThred の finished() シグナルの送信
  7. OS のスレッドの終了

QThread の run() が再実装されているか、あるいは QThread にイベントループを持たせるかどうかに関わらず、処理の流れ自体は変わりません。QThread のソースコードを参照すれば確認できますが、サンプルコードとデバッガーを使って実際に確認してみましょう。

サンプルコードでの確認

まず、QThread の started() シグナルと QThred の finished() シグナルの送信がサブスレッドで実行されていることを確認してみましょう。

#include <QCoreApplication>
#include <QThread>
#include <QTimer>
#include <QDebug>

using namespace std::chrono_literals;

void print(QAnyStringView message)
{
    qDebug().noquote().nospace() << message << ": isMainThread() = " << QThread::isMainThread();
}

int main(int argc, char** argv)
{
    QCoreApplication app(argc, argv);

    QThread thread;

    QObject::connect(&thread, &QThread::started, [] { print("started"); });
    QObject::connect(&thread, &QThread::finished, [] { print("finished"); });

    thread.start();

    QTimer::singleShot(1ms, [&thread] {
                                thread.quit();
                                thread.wait();
                                QCoreApplication::instance()->quit();
                            });

    return app.exec();
}

実行すると以下のように表示されます。

started: isMainThread() = false
finished: isMainThread() = false

QThread の started() シグナルおよび QThread の finished() シグナルに接続されたラムダ式は、各シグナルが送信されるスレッドと同じスレッドで実行されるため、これらのシグナルがサブスレッドで送信されていることが確認できます。

デバッガーでの確認

次に、デバッガーを使用して QThread の started() と QThread の run() にブレークポイントを設定し実行すると、run() の実行前に QThread の started() シグナルが送信されることが確認できます。また、OS スレッドが作成された後、そのスレッドにおいて QThread の started() シグナルが送信されることも確認できます。

$ lldb startedandfinished
(lldb) b QThread::started
(lldb) b QThread::run
(lldb) r

ブレークポイントを設定して実行します。

* thread #5, name = 'QThread', stop reason = breakpoint 1.1
    frame #0: 0x0000000100dde7e6 QtCore`QThread::started(this=0x00007ff7bfefedf0, _t1=QPrivateSignal @ 0x000070000a7bff38) at moc_qthread.cpp:193:18 [opt]

(lldb) bt
* thread #5, name = 'QThread', stop reason = breakpoint 1.1
  * frame #0: 0x0000000100dde7e6 QtCore`QThread::started(this=0x00007ff7bfefedf0, _t1=QPrivateSignal @ 0x0000700008a64f38) at moc_qthread.cpp:193:18 [opt]
    frame #1: 0x0000000100e8573c QtCore`QThreadPrivate::start(void*) [inlined] QThreadPrivate::start(void*)::$_1::operator()(this=<unavailable>) const at qthread_unix.cpp:331:19 [opt]
    frame #2: 0x0000000100e85649 QtCore`QThreadPrivate::start(void*) [inlined] void (anonymous namespace)::terminate_on_exception<QThreadPrivate::start(void*)::$_1>(t=<unavailable>) at qthread_unix.cpp:264:9 [opt]
    frame #3: 0x0000000100e85649 QtCore`QThreadPrivate::start(arg=0x00007ff7bfefedf0) at qthread_unix.cpp:295:5 [opt]
    frame #4: 0x00007ff80f62c1d3 libsystem_pthread.dylib`_pthread_start + 125
    frame #5: 0x00007ff80f627bd3 libsystem_pthread.dylib`thread_start + 15

QThread の started() で停止しました。OS スレッド作成後に QThread のstarted() が実行されています。

(lldb) thread list
Process 39119 stopped
  thread #1: tid = 0x7518ce, 0x0000000100cea1a0 QtCore`QArrayDataPointer<QEventLoop*>::reallocateAndGrow(this=0x0000600003308010, where=GrowsAtEnd, n=1, old=0x0000000000000000) at qarraydatapointer.h:220, queue = 'com.apple.main-thread'
  thread #2: tid = 0x7518ea, 0x00007ff80f5eec3e libsystem_kernel.dylib`__workq_kernreturn + 10
  thread #3: tid = 0x7518eb, 0x00007ff80f5eec3e libsystem_kernel.dylib`__workq_kernreturn + 10
  thread #4: tid = 0x7518ec, 0x00007ff80f3217e8 dyld`dyld3::MachOLoaded::findClosestSymbol(unsigned long long, char const**, unsigned long long*) const + 528
* thread #5: tid = 0x7518ed, 0x0000000100dde7e6 QtCore`QThread::started(this=0x00007ff7bfefedf0, _t1=QPrivateSignal @ 0x0000700008a64f38) at moc_qthread.cpp:193:18, name = 'QThread', stop reason = breakpoint 1.1

QThread の started() がサブスレッドで実行されています。

(lldb) c
* thread #5, name = 'QThread', stop reason = breakpoint 2.1
    frame #0: 0x0000000100dde174 QtCore`QThread::run(this=0x00007ff7bfefedf0) at qthread.cpp:742:12 [opt]
(lldb)

QThread の started() の送信後に QThread の run() が実行されています。

同様に、QThread の finished() シグナルについても、OS スレッド上で run() の実行後に送信されることがデバッガーで確認できます。

以上の結果から、QThread の started() および QThread の finished() シグナルの送信タイミングが明確になりました。

QThread の started() および QThread の finished() シグナルの使用例

いくつかの使用例において、QThread の started() シグナルおよび finished() シグナルに接続されたスロットが、どのスレッドで、またどのタイミングで実行されるかを確認します。

Controller と Worker の生成および破棄

Qt のドキュメントに掲載されているコード例を動作するように修正したものです。Controller と Worker のオブジェクトは new によりメモリ確保され、実行終了時にメモリが解放されるようにしています。実際のアプリケーションで使用する場合も、このように実装することになるでしょう。

#include <QCoreApplication>
#include <QThread>
#include <QTimer>
#include <QDebug>

using namespace std::chrono_literals;

void print(QAnyStringView message)
{
    qDebug().noquote().nospace() << message << ": isMainThread() = " << QThread::isMainThread();
}

class Worker : public QObject
{
    Q_OBJECT

public:
    explicit Worker(QObject* parent = nullptr) : QObject(parent) {
        print(Q_FUNC_INFO);
    }

    ~Worker() {
        print(Q_FUNC_INFO);
    }

public slots:
    void doWork(const QString& parameter) {
        print(Q_FUNC_INFO);
        qDebug().nospace() << "    parameter = " << parameter;

        QString result;
        result = parameter + ": result";
        emit resultReady(result);
    }

signals:
    void resultReady(const QString& result);
};

class Controller : public QObject
{
    Q_OBJECT

public:
    explicit Controller(QObject* parent = nullptr) : QObject(parent) {
        print(Q_FUNC_INFO);

        Worker* const worker = new Worker;
        worker->moveToThread(&workerThread);

        connect(&workerThread, &QThread::finished, worker, &QObject::deleteLater);
        connect(this, &Controller::operate, worker, &Worker::doWork);
        connect(worker, &Worker::resultReady, this, &Controller::handleResults);

        workerThread.start();
    }

    ~Controller() {
        print(Q_FUNC_INFO);

        workerThread.quit();
        workerThread.wait();  // Since workerThread is destroyed upon exiting this destructor.
    }

public slots:
    void handleResults(const QString& result) {
        print(Q_FUNC_INFO);
        qDebug().nospace() << "    result = " << result;
    };

signals:
    void operate(const QString&);

private:
    QThread workerThread;
};

int main(int argc, char** argv)
{
    print(Q_FUNC_INFO);

    QCoreApplication app(argc, argv);

    const auto controller = new Controller{};

    QTimer::singleShot(1ms, [controller] { emit controller->operate("Operate");});
    QTimer::singleShot(2ms, [controller] { delete controller;});
    QTimer::singleShot(20ms, [] { QCoreApplication::instance()->quit();});

    return app.exec();
}

#include "main.moc"

実行結果です。

$ ./deletecontrollerandworker
int main(int, char **): isMainThread() = true
Controller::Controller(QObject *): isMainThread() = true
Worker::Worker(QObject *): isMainThread() = true
void Worker::doWork(const QString &): isMainThread() = false
    parameter = "Operate"
void Controller::handleResults(const QString &): isMainThread() = true
    result = "Operate: result"
virtual Controller::~Controller(): isMainThread() = true
virtual Worker::~Worker(): isMainThread() = false
$

Worker のスロットはサブスレッド上で実行され、実行終了時に Controller とWorkerのオブジェクトがメモリ解放されていることが確認できます。

まとめ

QThread の started() および finished() シグナルはサブスレッドから送信され、接続されたスロットは接続方式に応じて異なるスレッドで実行される可能性があります。これらの挙動を正しく理解し適切に扱うことで、スレッド間の処理の安定性を高めることができます。次回は、今回の最後に示したサンプルコードを単純化し、その動作を確認します。

サンプルコード

startedandfinished.zip