前回に引き続き、Qtのsignal/slotとthreadの話。
と言っても、メインのスレッドとQThreadで作成したスレッドで、同じデータを触りたいときは、普通の並行プログラミングと変わらない。
QtのAPIのドキュメントに、thread-safeと書いてない限りは、QMutex等を使って自分で排他をする必要がある。
しかし、Qtを使っている場合は、わざわざ自分でMutexの管理をしなくても、スレッドとのデータのやり取りを全て signal/slotでやってしまい、共有データを持たないようにすれば、Mutexの出番はないのだ。
そこで、今回はsignal/slotでやり取りできるデータについて書いていくことにする。
ソースは、githubに上げてある。
int
いわゆるJavaで言うプリミティブタイプ。C++だとintとかlongとかdoubleとか。
こう言った型は、値渡しでコピーされるので、何も気にしなくて良い。
emit intResult(1);
connect(t, &MyThread::intResult, this, [](int result) {
qDebug() << QThread::currentThreadId() << "intResult:" << result;
});
QString
この例では、const QString &
にしてあるが、QString
でもあまり変わらない。(コストは・・・。QStringはmutableなので、使い方によっては違いがある・・・、けどそんな使い方しないよね?)
QString r("abc");
emit stringResult(r);
connect(t, &MyThread::stringResult, this, [](const QString &result) {
qDebug() << QThread::currentThreadId() << "stringResult:" << result;
});
MyObject
自前のクラス(QObjectのサブクラスでない)のオブジェクト。
MyObject o;
o.i = 3;
emit objectResult(o);
connect(t, &MyThread::objectResult, this, [](MyObject result) {
qDebug() << QThread::currentThreadId() << "objectResult:" << result.i;
});
自前のオブジェクトをsignal/slotで渡すときには、上記のようにそのまま書くとうまくいかない。
Q_DECLARE_METATYPE(MyObject)
qRegisterMetaType<MyObject>();
上記二つのおまじないが必要で、かつ自前のクラスは以下の条件を満たす必要がある。
- publicなデフォルトコンストラクタを持つ
- publicなコピーコンストラクタを持つ
- publicなデストラクタを持つ
ここまでの条件が必要な上、emitで1回、slotの呼び出しで1回のコピーが発生するので、今回のサンプルでは4回デストラクタが呼ばれている。(参照にすれば2回で済むかも?)
よほどの事情がない限り、この形を使うことはないだろう。
MyObject*
オブジェクトのコピーを嫌うのであれば、オブジェクトのポインタを渡せば良い。この場合は、MetaTypeの登録も不要である。
MyObject *p = new MyObject;
p->i = 5;
emit object_pResult(p);
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を使っても動くと思う。
QSharedPointer<MyObject> sp(new MyObject);
sp->i = 7;
emit object_spResult(sp);
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()
周りを読んで欲しい。
気が向いたら書くと思うが、続きではなく別の機会に。