(疑似)コード
これは、私が問題を抱えている概念のコンパイル不可能なコードスケッチです。
struct Data {};
struct A {};
struct B {};
struct C {};
/* and many many more...*/
template<typename T>
class Listener {
public:
Listener(MyObject* worker):worker(worker)
{ /* do some magic to register with RTI DDS */ };
public:
// This function is used ass a callback from RTI DDS, i.e. it will be
// called from other threads when new Data is available
void callBackFunction(Data d)
{
T t = extractFromData(d);
// Option 1: direct function call
// works somewhat, but shows "QObject::startTimer: timers cannot be started
// from another thread" at the console...
worker->doSomeWorkWithData(t); //
// Option 2: Use invokeMethod:
// seems to fail, as the macro expands including '"T"' and that type isn't
// registered with the QMetaType system...
// QMetaObject::invokeMethod(worker,"doSomeGraphicsWork",Qt::AutoConnection,
// Q_ARG(T, t)
// );
// Option 3: use signals slots
// fails as I can't make Listener, a template class, a QObject...
// emit workNeedsToBeDone(t);
}
private:
MyObject* worker;
T extractFromData(Data d){ return T(d);};
};
class MyObject : public QObject {
Q_OBJECT
public Q_SLOTS:
void doSomeWorkWithData(A a); // This one affects some QGraphicsItems.
void doSomeWorkWithData(B b){};
void doSomeWorkWithData(C c){};
public:
MyObject():QObject(nullptr){};
void init()
{
// listeners are not created in the constructor, but they should have the
// same thread affinity as the MyObject instance that creates them...
// (which in this example--and in my actual code--would be the main GUI
// thread...)
new Listener<A>(this);
new Listener<B>(this);
new Listener<C>(this);
};
};
main()
{
QApplication app;
/* plenty of stuff to set up RTI DDS and other things... */
auto myObject = new MyObject();
/* stuff resulting in the need to separate "construction" and "initialization" */
myObject.init();
return app.exec();
};
実際のコードからの詳細:
Listener
例の はRTI DataReaderListenerで、コールバック関数はonDataAvailable()
実現したいこと
通信にRTI の Connext DDSを使用し、GUI にQt5を使用する小さな分散プログラムを作成しようとしていますが、私が理解している限り、これらの詳細が問題として重要であるとは思いません。以下:
- スレッド アフィニティがメインの GUI スレッドにある場合とない場合がある QObject 派生オブジェクト
myObject
があります (ただし、簡単にするために、そうであると仮定しましょう)。 - そのオブジェクトが別の Qt 以外のサードパーティ ライブラリで発生するイベントに反応するようにしたい (上記のコード例では functions で表されます)
doSomeWorkWithData()
。
なぜこれが問題なのかについて私がこれまでに理解していること
免責事項:いつものように、新しいプロジェクトを開始するときに、常に複数の新しいことを学びます。私にとって、ここでの新しいものは RTI の Connext であり、(どうやら) 私自身がスレッドを処理しなければならないのは初めてのことです。
Qt ( 1、2、3、4、および5 ) のスレッド化について読むと、私にはそう思われます
- QObjects は一般にスレッドセーフではありません。つまり、少し注意が必要です。
- QObjects との「通信」の正しい方法を使用すると、ミューテックスなどを自分で処理する必要がなくなります。つまり、他の誰か (Qt?) がアクセスのシリアル化を処理してくれます。
その結果、単に (ランダムな) 呼び出しを行うことはできませんが、MyClass::doSomeWorkWithData()
それをシリアル化する必要があります。おそらく簡単な方法の 1 つは、イベント キュー ライブにイベントを投稿することですmyObject
。この場合、時間があるときに、目的のメソッドの実行がトリガーさMyClass::doSomeWorkWithData()
れます。
物事を機能させるために私が試みたこと
myObject
上記のサンプル コードと同様に をインスタンス化すると、メインの GUI スレッド、つまりmyObject.thread() == QApplication::instance()->thread()
.
そのため、これまでに次の 3 つのオプションを試しました。
オプション 1: 関数を直接呼び出す
このアプローチは、次の事実に基づいていmyObject
ます - GUI スレッドに存在する - 作成されたすべてのリスナーは、'myObject' によって作成され、そのスレッドを継承するため、GUI スレッドにも関連付けられます。
これは実際にdoSomeWorkWithData()
が実行されるという結果になります。ただし、これらの関数の一部は QGraphicsItems を操作し、その場合は常に「QObject::startTimer: タイマーを別のスレッドから開始することはできません」というエラー メッセージが表示されます。
オプション 2: 経由でイベントを投稿するQMetaObject::invokeMethod()
のイベントを適切に投稿することでこの問題を回避しようとしてmyObject
、 でマークしようとしましMyObject::doSomeWorkWithData()
たQ_INVOKABLE
が、 で引数を渡す必要があるため、メソッドの呼び出しに失敗しましたQ_ARG
。などで表されるカスタム型を適切に登録して宣言しましたが、引数の型のリテラルを含めるように拡張されstruct A
たという事実に失敗しました
。登録または宣言された型)。Q_ARG
"T"
従来のシグナルとスロットを使用しようとしている
このアプローチは、QMeta システムがテンプレートで機能しないという事実で本質的に直接失敗しました。つまり、テンプレート化された QObject はまったくあり得ないように思えます。
助けてほしいこと
これを修正するために約 1 週間を費やし、スレッドを調べた後 (そしてコード内の他の問題を明らかにした後)、これを正しく完了させたいと思っています。そのため、次の場合は本当に感謝します:
誰かが、別の非 QThread 制御のスレッドから、別のサードパーティ ライブラリ (またはその他のもの) からコールバック関数を介して QObject のメンバー関数を呼び出す方法の一般的な方法を教えてくれます。
GUIを作成しない場合、つまり、QGraphcisSceneを視覚化せずにすべて同じ作業を行う場合に、オプション1が機能する理由を誰かが説明できます(そして、プロジェクト
app
はaQCoreApplication
ではなく、QApplication
すべてのグラフィックス関連の作業が行わ#define
れます) .
私がつかむことができるすべての、つまり絶対にすべてのストローは本当に感謝しています.
アップデート
void doSomeWorkWithData()
受け入れられた回答に基づいて、他のスレッドからのコールバックを処理するようにコードを変更しました。関数の先頭にスレッド チェックを導入しました。
void doSomeWorkWithData(A a)
{
if( QThread::currentThread() != this->thread() )
{
QMetaObject::invokeMethod( this,"doSomeWorkWithData"
,Qt::QueuedConnection
,Q_ARG(A, a) );
return;
}
/* The actual work this function does would be below here... */
};
関連する考え:
QMutexLocker
私はステートメントの前にa を導入することを考えていましたが、それをやめるif
ことにしました: 並列で使用される可能性のある関数の唯一の部分 (ステートメントの上記の部分return;
)if
は、私が理解している限り、スレッドセーフです。接続タイプを手動で に設定する
Qt::QueuedConnection
:技術的には、ドキュメントを正しく理解していれば、Qt は正しいことを行うはずであり、デフォルトのQt::AutoConnection
は最終的に になりますQt::QueuedConnection
。しかし、そのステートメントに到達した場合は常にそうであるため、なぜこれが存在するのかを思い出すために、明示的にそこに入れることにしました。invokeMethod
キューイングコードを関数に直接配置し、暫定関数に隠しません。呼び出しを別の暫定関数に入れることを選択できました。たとえば、queueDoSomeWorkWithData()', which would be called by the callback in the listener and then uses
invokeMethodwith an
Qt::AutoConnection' ondoSomeWorkWithData()
です。テンプレートを介してこの中間関数を自動コーディングする方法がないように思われるため(テンプレートとメタシステムは元の問題の一部でした)、コードの「ユーザー」(つまり、実装する人doSomeWorkWithData(XYZ xyz)
)は中間関数も手動で入力する必要があります(テンプレート化された型名が正しく解決される方法です)。実際の関数にチェックを含めることは、追加の関数ヘッダーを安全に入力できるように思われ、MyClass
インターフェースを少しきれいに保ち、読者に次のことをよりよく思い出させますdoSomeWorkWithData()
スレッドの問題が潜んでいる可能性があります。