Qt5のQTimerを他スレッドから呼び出していることに気が付かず苦心したのでメモっておきました

[Qt](https://doc.qt.io/qt-5/)を使ったプログラムでタイマ（[QTimer](https://doc.qt.io/qt-5/qtimer.html)）を使ったら次のような警告が表示されました。このメッセージだけでなくタイマも有効になりませんでした。

```text
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クラスはタイマを使うクラスです。

```c++
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);
}

```

[公式マニュアル](https://doc.qt.io/qt-5/qtimer.html)には次のように書かれています。

```
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`シグナルの存在意義を理解できた気がします。

```c++
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できるようでした。最初に示した問題はおきず期待通りに動きます。

先の方法に比べてコードはシンプルなので使いたくなりますが、もし邪道なら、たまたま動いているのかもしれないので避けた方がいいかもしれません。

```c++
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シグナルのスロットで破棄する。

# 補足

## 参考にしたウェブページ

* [QTimer](https://doc.qt.io/qt-5/qtimer.html)
* [Qtのsignal/slotとthread(3)](https://qiita.com/false-git@github/items/38b6e69557b8459e9c0e)

## 環境

* Ubuntu 18.04
* GNU 4.8.5
* Qt 5