2026年7月5日日曜日

QtのQThreadとQTimerについて

QtつかっててQThreadとタイマー使ってると以下のエラーが出ることがある。

QObject::~QObject: Timers cannot be stopped from another thread

何がなんだか分からないまま闇雲になんとかしてたけど、なんか気持ち悪いので調べ直した。素人調べなので間違っているかもしれない。

QThreadの使い方

そもそもQThreadがどういうものなのか?調べた限りでは「slotで動かしたものがスレッドで動く」という仕組みのもののようだ。

class Worker : public QObject
{
/* 略 */
public slots:
    void exec();
    void exec2();
};

こんなのがあったとして、MainWindowから使う場合、

// worker作る
auto obj = new Worker();

// スレッドに移動
auto thr = new QThread();
obj->moveToThread(thr);
thr->start();

obj->exec(); // MainWindowのスレッドで動く
obj->exec2(); // MainWindowのスレッドで動く

// connect & emit
connect(this,&MainWindow::exec,obj,&Worker::exec);
connect(this,&MainWindow::exec2,obj,&Worker::exec2);

emit exec(); // Workerのスレッドで動く
emit exec2(); // Workerのスレッドで動く

emitでexec,exec2をslotで動かした場合にWorkerスレッドで動く 。execが終わるまではexec2は動かないようなので、同一スレッド内では通常は同時に動かないっぽい。

※ ただし、QEventloopでイベントを受け付けたりするとその限りではないようだ。QTcpClientとかのwaitForConnect中とかでも他のスロットが動いたりした。内部でイベントループがあるのかもしれない。同時に動いて困る場合は念の為QMutexとかで排他したほうが良いだろう。

サンプルで、startedのsignalにconnectしたりしてるのをよく見る。下記の例だと単にthr->startを実行する時にexecも実行するというだけでしかない。

ちなみにobj->quit()したときにスロットを実行するsignalでfinishedというのもある。
以下だとexecは2回動く(はず)。

connect(thr, &QThread::started, obj, &Worker::exec);
connect(thr, &QThread::finished, obj, &Worker::exec);
thr->start();
thr->quit();

QThreadを継承する場合

WorkerをQThreadの子クラスにするやり方もあるようだが、上記の使い方だとそんなに違いはない。

class Worker : public QThread
以下略
// worker作る
auto obj = new Worker();
// スレッドに移動
obj->moveToThread(obj);
obj->start();

obj->exec(); // MainWindowのスレッドで動く

// connect & emit
connect(this,&MainWindow::exec,obj,&Worker::exec);

emit exec(); // QThreadのスレッドで動く

QThreadを継承しても、moveToThreadするまではMainWindowのスレッドで動くっぽい。moveToThreadしないとQThreadの意味はない。上記はMainWindow側でmoveToThreadをやっているが、Workerのコンストラクタでやっても良さそう。

この使い方なら別でスレッドオブジェクトを作らなくていいくらいしか違いはなさそうだが、runをオーバーライドする場合は違いがある。

class Worker : public QThread
{
/* 略 */
void run() override;
};

この場合はthr->start();するだけでrunが実行され、関数終了でsignal finishedが発行される。よく見る例で便利そうに見えるが、run以外の処理を実行するにはQEventloopを使わないといけない。関数一つだけ別スレッドで動かすやり方のようだ。関数一つだけのためにクラスを作らないといけない。こういう用途だとQtConcurrent使ったほうが簡単じゃないのか。個人的にはあんまり実用的じゃない気がする。

QThreadでタイマーを使うとき

さて、このQThread上でタイマーを使おうとしたとき、 「Timers cannot be stopped from another thread」と出てきて、なんでだよこのクソ野郎が!と思うことが稀によくある。

以下のような場合だ。QtのタイマーはQObjectに備え付けのstartTimer/killTimerとQTimerのstart/stopを使ってみている。

class Worker : public QThread
{
    Q_OBJECT
    QTimer mtimer;
    int m_id;
    void timerEvent(QTimerEvent *event) override;
public slots:
    void exec();
/* 略 */
};

Worker::Worker(QObject *parent)
    : QThread{parent}
{
    m_id = startTimer(200);
    connect(&mtimer,&QTimer::timeout,this,&Worker::exec);
    mtimer.start(200);
}

Worker::~Worker()
{
    mtimer.stop();
    killTimer(m_id);
}

MainWindowから以下のように使う

    auto obj = new Worker();
    obj->moveToThread(obj);
    // start thread
    obj->start();
    QThread::msleep(1000);

    // end
    qDebug() << "stop thread";
    obj->quit();
    obj->wait();
    qDebug() << "delete worker";
    delete obj;

deleteするときにmtimerは止められるが、killTimerはエラーになる。 逆にQThread上でタイマーを止めようとするとmtimerは止められず、killTimerは止められる。mtimerはWorkerのスレッドではなく、コンストラクタを動かしたMainWindowのスレッドで動いているということになる。

moveThreadでobjはQThreadに移動されたが、その中のmtimerは移動してくれなかったということだ。 

MainWindowからしかstart/stopしないという使い方の場合はむしろこのままmtimerを使うというのでも良いのかもしれないが、以下のようにmtimer作成時に親を自分に設定すると、moveThreadのときに一緒に子どもであるmtimerも動いてくれる。
そうでなければコンストラクタを動かしたMainWindowが親になってしまう。

Worker::Worker(QObject *parent)
    : QThread{parent},mtimer(this)
{
/* 略 */

これでスレッドの中ではstart/stopができるようになるが、やはりデストラクタでは止められない。むしろkillTimerもmtimer.stopも両方NGになる。

結局はこれらもデストラクタでストップするのではなく、スレッドの中で止めなければならないということだ。停止するslotを作成し、slotで止めてからdeleteすれば問題ない。

    connect(this,&MainWindow::kikktimer,obj,&Worker::killtimer);
    connect(this,&MainWindow::mtimer_stop,obj,&Worker::mtimer_stop);

    // stop timer
    emit mtimer_stop(); // タイマーを止めるslot
    emit killtimer(); // タイマーを止めるslot
    QThread::msleep(1000);

    // end
    qDebug() << "stop thread";
    obj->quit();
    obj->wait();
    qDebug() << "delete worker";
    delete obj;

runをオーバーライドしている場合はイベントループとか使わないと別のスロットを動かせない。何かフラグを立てて、run関数の中で止めるか、finishedシグナルでrun関数が終わったあとに何とかするしかない。 

finishedで後始末 

runをオーバーライドしているとか、面倒くさいからスレッドが終わったら止まれよ。という人向けにfinishedシグナルがあるんだろう。finishedでタイマーを止める。

    connect(obj,&QThread::finished,obj,&Worker::killtimer);
    connect(obj,&QThread::finished,obj,&Worker::mtimer_stop);
    // end
    qDebug() << "stop thread";
    obj->quit();
    obj->wait(); // この辺でタイマーが止まる
    qDebug() << "delete worker";
    delete obj; 

終わったらdeleteもやれよという人にはdeleteLaterがあるが、ちょっと罠がある。 

    connect(obj,&QThread::finished,obj,&Worker::killtimer);
    connect(obj,&QThread::finished,obj,&Worker::mtimer_stop);
    connect(obj,&QThread::finished,obj,&Worker::deleteLater);
    // end
    qDebug() << "stop thread";
    obj->quit();
    // obj->wait(); // これやると死ぬ

obj->quit()した時点でkillTimer/mtimer_stop/deleteがWorkerスレッドのタイミングで始まる。MainWindowスレッドのobj->waitしている間にdeleteまでされるので、obj->wait実行中にobjが消えることになる。finishedでdeleteLaterを呼ぶ場合はquitしたらもうobjには触ってはいけないということだろう。 

一回スタートしたらあとはsignal/slotで自動で消えてくれる。Qtをうまく活用した処理と言えるけど、個人的にはスレッドが終わるのを確認しないと気持ち悪いし、ちゃんと自分でdeleteしないと気持ち悪いんだけどな。