QThread + QtSqlの正しい方法

今日の記事に触発されて、データベースを別のスレッドに配置する方法を共有しました。 このメソッドは、データベースだけでなく、「一部のオブジェクトは別のストリームに住んでいるので、何かを尋ねてそれを使って何かをする必要がある」というパターンで説明されるやり取りにも適しています。 さらに、このメソッドは、タイプセーフで拡張可能であろうという点で優れています。文字列型のQMetaObject::invokeMethod() 、シグナルを介したストリームのQMetaObject::invokeMethod()の結果の送信はありません。 直接関数呼び出しのみ、 QFutureのみ!

免責事項:ここで与えられたコードは私の大きなプロジェクトの一部であるため、このプロジェクトの補助ライブラリ関数を使用します。 ただし、このような使用例を見逃さないようにし、それらのセマンティクスを説明します。

それでは、最も重要なことから始めましょう。別のスレッドでオブジェクトをどのように処理したいのでしょうか? 理想的には、オブジェクトからメソッドをプルするだけで、メソッドはQFuture<T>返します。その準備ができていることは、対応する非同期メソッドの実行が完了し、 Tような結果になることを意味します

分解は私たちの友人であることを思い出してください。元のタスク「別のスレッドに何かをプルする必要があります」を検討して、その部分を「別のスレッドを保持し、 QFuture returnでスレッドセーフな呼び出し提供する必要がある」と考えてみQFutureう。

この問題を次のように解決します:スレッド制御を担当するQThread子孫QThreadには、メインスレッド(および他のスレッド)から呼び出されるScheduleImpl()メソッドがあり、このファンクターをQFutureでラップして必要なすべてを保存するファンクターを受け入れます。 QThread::run()内で処理される特別なキューに移動します。

次のようなものが得られます。
 class WorkerThreadBase : public QThread { Q_OBJECT QMutex FunctionsMutex_; QList<std::function<void ()>> Functions_; public: using QThread::QThread; protected: void run () override; virtual void Initialize () = 0; virtual void Cleanup () = 0; template<typename F> QFuture<ResultOf_t<F ()>> ScheduleImpl (const F& func) { QFutureInterface<ResultOf_t<F ()>> iface; iface.reportStarted (); auto reporting = [func, iface] () mutable { ReportFutureResult (iface, func); }; { QMutexLocker locker { &FunctionsMutex_ }; Functions_ << reporting; } emit rotateFuncs (); return iface.future (); } private: void RotateFuncs (); signals: void rotateFuncs (); }; 


あらゆる種類のReportFutureResultおよびResultOf_tの説明
ResultOf_tは、C ++ 14のstd::result_of_t直接類似しています。 残念ながら、私のプロジェクトはまだC ++ 11コンパイラをサポートする必要があります。
 template<typename T> using ResultOf_t = typename std::result_of<T>::type; 


ReportFutureResultはファンクターとその引数を取り、ファンクターを実行し、対応するQFutureInterfaceを準備完了としてマークし、同時にファンクター実行の結果を渡します。または、ファンクターがこの例外で完了した場合、 QFutureInterface例外をラップQFutureInterfaceます。 残念なことに、この問題はvoidファンクタを返すことによってやや複雑です。C++ではvoid型の変数を宣言できないため、別の関数を記述する必要がありvoid 。 このような型のシステムがあります。ああ、型があり、その中に値がありますが、宣言することはできません。
 template<typename R, typename F, typename... Args> EnableIf_t<!std::is_same<R, void>::value> ReportFutureResult (QFutureInterface<R>& iface, F&& f, Args... args) { try { const auto result = f (args...); iface.reportFinished (&result); } catch (const QtException_t& e) { iface.reportException (e); iface.reportFinished (); } catch (const std::exception& e) { iface.reportException (ConcurrentStdException { e }); iface.reportFinished (); } } template<typename F, typename... Args> void ReportFutureResult (QFutureInterface<void>& iface, F&& f, Args... args) { try { f (args...); } catch (const QtException_t& e) { iface.reportException (e); } catch (const std::exception& e) { iface.reportException (ConcurrentStdException { e }); } iface.reportFinished (); } 


QtException_tビルドをサポートするにQtException_t必要です。
 #if QT_VERSION < 0x050000 using QtException_t = QtConcurrent::Exception; #else using QtException_t = QException; #endif 


ConcurrentStdException 、標準の例外をQtのQFutureメカニズムが理解できるものにラップしますが、その実装はもう少し複雑であり、ここではそれほど重要ではありません。


つまり、 ScheduleImpl()は、タイプT ()シグネチャを持つ特定のファンクターを受け入れ、 QFuture<T>返し、ファンクターを特別な関数でラップし、返されたQFuture<T>に関連付けられたシグネチャvoid () 、およびこのファンクターに関連付けられvoid () QFuture<T>それを準備完了としてマークし、このラッパーをキューに追加します

その後、信号rotateFuncs()され、 run()内でRotateFuncs()メソッドに接続されます。このメソッドは、格納されているファンクターのラッパーのキューを処理するだけです。

run()およびRotateFuncs()メソッドの実装を見てみましょう。
 void WorkerThreadBase::run () { SlotClosure<NoDeletePolicy> rotator { [this] { RotateFuncs (); }, this, SIGNAL (rotateFuncs ()), nullptr }; Initialize (); QThread::run (); Cleanup (); } void WorkerThreadBase::RotateFuncs () { decltype (Functions_) funcs; { QMutexLocker locker { &FunctionsMutex_ }; using std::swap; swap (funcs, Functions_); } for (const auto& func : funcs) func (); } 


SlotClosureについて少し
SlotClosureは、スロットではなくラムダにシグナルをアタッチするのに役立つヘルパークラスです。 Qt5には、このためのより適切な構文がありますが、残念ながら、Qt4アセンブリもサポートする必要があります。

SlotClosureは単純SlotClosure番目の引数であるオブジェクトが3番目の引数であるシグナルをSlotClosureするたびに、最初の引数を呼び出します。 4番目の引数は親オブジェクトです。 ここでは、スタックにSlotClosure設定されているため、親は必要ありません。

テンプレート引数NoDeletePolicyは、最初の信号の後にオブジェクトが自殺しないことを意味します。 他の削除ポリシーには、たとえば、 DeleteLaterPolicyもあります。これは、信号の最初の操作後に接続オブジェクトをDeleteLaterPolicyします。これは、一度実行されるさまざまなタスクに便利です。


これらの関数はすべてシンプルです: rotateFuncs()信号をrotateFuncs()関数に接続します(うーん、命名スタイルにいくつのコメントがあるのでしょうか?)、ストリームオブジェクトの初期化関数を呼び出し、後継のどこかで定義し、ストリームをねじり始めます。 スレッドの所有者がスレッドに対してquit()を行うと、 QThread::run()が制御を返し、相続人はCleanup()でそれをクリーンアップできます。

メインスレッドからrotateFuncs()されたRotateFuncs()WorkerThreadBase RotateFuncs()RotateFuncs()ことを保証するのは、Qtのシグナルスロットメカニズムです。

ただし、 RotateFuncs() 、メインキューを一時的にブロックし、それ自体に移動した後、順次実行を開始します。

実際、それだけです。 使用例として、たとえば、IMクライアントのディスク上のアバター用のストレージシステムの一部を引用できます。
avatarsstoragethread.h
 class AvatarsStorageThread final : public Util::WorkerThreadBase { std::unique_ptr<AvatarsStorageOnDisk> Storage_; public: using Util::WorkerThreadBase::WorkerThreadBase; QFuture<void> SetAvatar (const QString& entryId, IHaveAvatars::Size size, const QByteArray& imageData); QFuture<boost::optional<QByteArray>> GetAvatar (const QString& entryId, IHaveAvatars::Size size); QFuture<void> DeleteAvatars (const QString& entryId); protected: void Initialize () override; void Cleanup () override; }; 


avatarsstoragethread.cpp
 QFuture<void> AvatarsStorageThread::SetAvatar (const QString& entryId, IHaveAvatars::Size size, const QByteArray& imageData) { return ScheduleImpl ([=] { Storage_->SetAvatar (entryId, size, imageData); }); } QFuture<boost::optional<QByteArray>> AvatarsStorageThread::GetAvatar (const QString& entryId, IHaveAvatars::Size size) { return ScheduleImpl ([=] { return Storage_->GetAvatar (entryId, size); }); } QFuture<void> AvatarsStorageThread::DeleteAvatars (const QString& entryId) { return ScheduleImpl ([=] { Storage_->DeleteAvatars (entryId); }); } void AvatarsStorageThread::Initialize () { Storage_.reset (new AvatarsStorageOnDisk); } void AvatarsStorageThread::Cleanup () { Storage_.reset (); } 



また、 AvatarsStorageOnDiskの実装もあります。これは、Boost.Fusionを介したデータ構造の説明に従って、タブレット、SQLクエリ、および挿入/削除/更新用の対応する関数を生成できる、独自のORMフレームワークに関連する別の興味深いトピックです。 ただし、この実装は、特に元の問題の分解の観点から、一般に良いマルチスレッドの問題に特に関係していません。

そして最後に、提案されたソリューションの欠点に注意してください。
  1. WorkerThreadBase後継WorkerThreadBaseパブリックAPIで、別のスレッドにWorkerThreadBaseれるオブジェクトで呼び出すすべてのメソッドを複製する必要があります。 この問題を効果的に解決する方法は、すぐには思いつきませんでした。
  2. Initialize()およびCleanup() 、何らかのRAIIに変換するように直接求められます。 このテーマについて何か考え出す価値はあります。

Source: https://habr.com/ru/post/J273743/


All Articles