Qtを使ったプログラムでタイマ(QTimer)を使ったら次のような警告が表示されました。このメッセージだけでなくタイマも有効になりませんでした。
QObject::killTimer: Timers cannot be stopped from another thread
QObject::startTimer: Timers cannot be started from another thread
原因は、QTime::start
またはQTime::stop
を実行するスレッドと、QTime
インスタンスが属するスレッドが異なっていたためでした。警告文が出たプログラムでは、QTime
インスタンスが属するスレッドはワーカスレッドであり、QTime::start
はメインスレッド(QApplication::exec()したスレッド)で実行していました。自分ではQTime::start
をワーカスレッドで実行していたつもりだったんですが勘違いでした。
問題コードのイメージは次です。Workerクラスはタイマを使うクラスです。
Worker::Worker(QObject *parent)
: QObject(nullptr), _thread(new QThread(parent)), _timer(nullptr)
{
_timer = new QTimer(this);
connect(_timer, &QTimer::timeout, this, &Worker::onTimeout);
_timer->setSingleShot(true);
moveToThread(_thread); // ワーカスレッドにする(スレッドを移動する)
}
void Worker::start()
{
_thread->start();
_timer->start(5000);
}
公式マニュアルには次のように書かれています。
You must start and stop the timer in its thread.
It is not possible to start a timer from another thread.
(タイマーの起動と停止はそのスレッドで行う必要があります。
他のスレッドからタイマーを起動することはできません。)
なので、_thread->start()
した後に_timer->start()
したのですが、これではダメだったようです。
解決策
解決策(王道?)
確実にワーカスレッドでQTimer::start()
させるため、QThread::started
シグナルのスロットでQTimer::start()
しました。QThread::start()
後だからといってワーカスレッドではないんですね。今更ですが、QThread::started
シグナルの存在意義を理解できた気がします。
Worker::Worker(QObject *parent)
: QObject(nullptr), _thread(new QThread(parent)), _timer(nullptr)
{
_timer = new QTimer(this);
connect(_timer, &QTimer::timeout, this, &Worker::onTimeout);
_timer->setSingleShot(true);
moveToThread(_thread); // ワーカスレッドにする(スレッドを移動する)
_timer->moveToThread(this->thread()); // タイマもワーカスレッドにする
}
void Worker::start()
{
_thread->start();
// _timer->start(5000);
connect(_thread, &QThread::started, this, &Worker::onThreadStarted);
}
/*
* QTimer::start/stopは別スレッドから呼ばない(別スレッドからのシグナルで呼ばない)
*/
void Worker::onThreadStarted()
{
_timer->start(5000);
}
解決策(邪道?)
先に示した方法はおそらく、QTimerの設計方針に則ったまっとうな使い方だと思っています。それとは別の解決方法も有りました。
この方法はどういう仕組みで問題がおきないのか未だ理解できていませんが、QTimerインスタンスからstart/stopするのではなく、invokeMethod
を使ってstart/stopを呼ぶと、どのスレッドで実行するか気にせずいつでも問題なくstart/stopできるようでした。最初に示した問題はおきず期待通りに動きます。
先の方法に比べてコードはシンプルなので使いたくなりますが、もし邪道なら、たまたま動いているのかもしれないので避けた方がいいかもしれません。
Worker::Worker(QObject *parent)
: QObject(nullptr), _thread(new QThread(parent)), _timer(nullptr)
{
_timer = new QTimer; // 親無し/moveToThread無し
connect(_timer, &QTimer::timeout, this, &Worker::onTimeout);
_timer->setSingleShot(true);
moveToThread(_thread); // ワーカスレッドにする(スレッドを移動する)
}
void Worker::onTimeout()
{
// サイドタイマをかける(〇〇をリトライする)
QMetaObject::invokeMethod(_timer, "start");
}
簡単なまとめ
- ワーカスレッドでQTimerを使う場合は、QTimerも同じスレッドに属するようmoveToThreadでQThreadオブジェクトを指定する
- QTimer::start/stopは別スレッドから呼ばせない
- 例えば最初にタイマを開始するためのQTimer::startは、QThread::startedシグナルのスロットで呼び出す等
- QTimerオブジェクトを破棄する際は(stopが呼ばれる?ので)QTimerの持ち主のデストラクタで破棄するのではなく、QThread::finishedシグナルのスロットで破棄する。
補足
参考にしたウェブページ
環境
- Ubuntu 18.04
- GNU 4.8.5
- Qt 5