LoginSignup
14
12

More than 5 years have passed since last update.

Qtのsignal/slotとthread(2)

Posted at

前回に引き続き、Qtのsignal/slotとthreadの話。

と言っても、メインのスレッドとQThreadで作成したスレッドで、同じデータを触りたいときは、普通の並行プログラミングと変わらない。
QtのAPIのドキュメントに、thread-safeと書いてない限りは、QMutex等を使って自分で排他をする必要がある。

しかし、Qtを使っている場合は、わざわざ自分でMutexの管理をしなくても、スレッドとのデータのやり取りを全て signal/slotでやってしまい、共有データを持たないようにすれば、Mutexの出番はないのだ。

そこで、今回はsignal/slotでやり取りできるデータについて書いていくことにする。

ソースは、githubに上げてある。

int

いわゆるJavaで言うプリミティブタイプ。C++だとintとかlongとかdoubleとか。
こう言った型は、値渡しでコピーされるので、何も気にしなくて良い。

mythread.h
        emit intResult(1);
mainwindow.cpp
    connect(t, &MyThread::intResult, this, [](int result) {
        qDebug() << QThread::currentThreadId() << "intResult:" << result;
    });

QString

この例では、const QString & にしてあるが、QStringでもあまり変わらない。(コストは・・・。QStringはmutableなので、使い方によっては違いがある・・・、けどそんな使い方しないよね?)

mythread.h
        QString r("abc");
        emit stringResult(r);
mainwindow.cpp
    connect(t, &MyThread::stringResult, this, [](const QString &result) {
        qDebug() << QThread::currentThreadId() << "stringResult:" << result;
    });

MyObject

自前のクラス(QObjectのサブクラスでない)のオブジェクト。

mythread.h
        MyObject o;
        o.i = 3;
        emit objectResult(o);
mainwindow.cpp
    connect(t, &MyThread::objectResult, this, [](MyObject result) {
        qDebug() << QThread::currentThreadId() << "objectResult:" << result.i;
    });

自前のオブジェクトをsignal/slotで渡すときには、上記のようにそのまま書くとうまくいかない。

mythread.h
Q_DECLARE_METATYPE(MyObject)
mainwindow.cpp
    qRegisterMetaType<MyObject>();

上記二つのおまじないが必要で、かつ自前のクラスは以下の条件を満たす必要がある。

  • publicなデフォルトコンストラクタを持つ
  • publicなコピーコンストラクタを持つ
  • publicなデストラクタを持つ

ここまでの条件が必要な上、emitで1回、slotの呼び出しで1回のコピーが発生するので、今回のサンプルでは4回デストラクタが呼ばれている。(参照にすれば2回で済むかも?)
よほどの事情がない限り、この形を使うことはないだろう。

MyObject*

オブジェクトのコピーを嫌うのであれば、オブジェクトのポインタを渡せば良い。この場合は、MetaTypeの登録も不要である。

mythread.h
        MyObject *p = new MyObject;
        p->i = 5;
        emit object_pResult(p);
mainwindow.cpp
    connect(t, &MyThread::object_pResult, this, [](MyObject *result) {
        qDebug() << QThread::currentThreadId() << "object_pResult:" << result->i;
    });

このやり方には問題がある。
new で確保したMyObjectは決してdeleteされない。
ポインタで受け渡す以上、メモリの管理をどちらが行うかは、決めておく必要がある。しかし、emitする側はいつslotが呼び出されるかわからないため、実質的に解放するタイミングがわからない。
必然的に、slot側でdeleteすることになるが、一つのsignalに複数のslotをconnectすることが可能なので、自分が一番最後に呼ばれたslotかどうかがわからないとやはりdeleteするタイミングがわからないことになる。
connectの引数で渡すConnectionTypeに、Qt::UniqueConnectionを||で指定すると、複数のslotがconnectされることを防げるが、connectされていない場合にはやはりリークが発生する。

QSharedPointer<MyObject>

QSharedPointerは、c++11のstd::shared_ptrとほぼ同じだと思って良い。試していないが、CONFIG += c++11 してあれば、QSharedPointerの代わりにstd::shared_ptrを使っても動くと思う。

mythread.h
        QSharedPointer<MyObject> sp(new MyObject);
        sp->i = 7;
        emit object_spResult(sp);
mainwindow.cpp
    connect(t, &MyThread::object_spResult, this, [](QSharedPointer<MyObject> result) {
        qDebug() << QThread::currentThreadId() << "object_spResult:" << result->i;
        result->i = 9;
    });
    connect(t, &MyThread::object_spResult, this, [](QSharedPointer<MyObject> result) {
        qDebug() << QThread::currentThreadId() << "object_spResult2:" << result->i;
    });

このケースでも、Q_DECLARE_METATYPE(QSharedPointer<MyObject>)の宣言と、qRegisterMetatype<QSharedPointer<MyObject>>()の呼び出しが必要になる。
上のサンプルでは、同じsignalに対して2回connectを実行している。
Qtのsignal/slotのルールは、複数connectした場合は、一つ一つ順番にconnectした順序でslotが呼び出される。
上のサンプルでは、わざわざ最初のslotでMyObjectの中身を変更しているが、2番目に呼ばれるslotでは変更後のオブジェクトにアクセスすることになる。
イベント伝搬の仕組みでも、イベントハンドラの中でQEvent::accept()とかでイベントの状態を変えたりするけど、slotをconnectの順番に依存したり、引数を変えたりするのはオススメしない。

後は、QObjectのサブクラスの場合、QWidgetのサブクラスの場合、QList等のQObjectのサブクラスでないQtのコンテナ型の場合、等いろいろ考えられるが、力尽きたので今回はここまで。

特に、QObjectには thread affinity と言う概念があり、別スレッドでnewしたオブジェクトは親子関係にすることができない。この辺については、QObjectのマニュアルのmoveToThread()周りを読んで欲しい。
気が向いたら書くと思うが、続きではなく別の機会に。

14
12
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
14
12