はじめに
Qtアドベントカレンダーもついに7日目になりました…ってかっこよく言いたかったのですが、だいぶ遅れてしまいすいません。
金曜日の夜は忘年会でして、例によって飲みながら書こうかと思ったら超オシャレな場所でそういう雰囲気でもなく。で、本日は微妙な咳が出て怪しかったので1日寝て過ごしてました。周りでインフルエンザだとマイクプラズマだの聞くと孤独老人に片足突っ込んでる身としては身の危険を感じるもので。
昨日は @task_jp さんの「Yocto の meta-qt6 の SDK を作成して Qt Creator に設定をする」でした。Qt Creatorで設定しておけばQt Creatorへのデプロイまで簡単にできるし、容量がたりなくてもNFS bootできるならQt Creatorでデバッグまでいけるのでおすすめです。
なお、チームで作業される方は、aqtinstallの記事であまり深く解説せずに出したsdktoolを使うスクリプトを作ってSDK install後に組込向けキットを登録するスクリプトを流すと環境がすぐに出来上がったりするので、鈴木さんの記事を読んで何を行う必要があるかわかったら、sdktoolに手を出してみるのも一興です。
本日のお題
どれにしようか悩んでましたが、体調を崩してしまったので、さっくりざっくり短めの記事ということで、スレッド関連の情報追加でお茶を濁そうかと思います。
はい、またお前スレッドかよってツッコミはなしの方向で一つ。なお過去記事一覧です。
上記記事で触れていなかった機能・追加機能
QRecursiveMutex
ミューテックスは、マルチスレッド環境でスレッドが共用リソースに同時アクセスしないよう排他制御するための仕組みです。しかし、このミューテックス同時にロックできるのは1つだけで、ロック中のスレッドであっても再ロックしようとするとデッドロックを起こします。
QMutex m_lock;
void funcA()
{
QMutexLocker lock(&m_lock);
:
}
void funcB()
{
QMutexLocker lock(&m_lock);
funcA();
}
funcBは同じミューテックスをロックするfuncAを呼び出すのでデッドロックになるわけですが、QMutexの変わりにQRecursiveMutexを使うと同一スレッドないなら何度でもロックできすべてのロックが解除されるまで保護することができるようになります。
ただし、一点注意が必要なのは、QMutexよりもオーバーヘッドが大きいためパフォーマンスに影響がでます。上記のような例では設計が悪いので治すほうが良いでしょう。
では、どんなときに有用なのかというと、スレッドの処理の再帰処理中などでリソース管理が必要なケースや、非常に複雑な処理中、特殊な条件下で必要など、コストが許容できる場合などに利用することになるでしょう。
QPromise
QPromiseは、QFutureとの間の非同期通信に使われるクラスです。QFutureは実装当初、QtConcurrent機構のスレッドの状態や結果を操作するクラスとして提供されました。QPromiseをつかうと通知のタイミングや進捗表示を変えたり、QThreadからであってもQFuture経由で制御できるようになります。以下は待ち合わせと結果取得の実装例です。
QPromise<int> promise;
QFuture<int> future = promise.future();
const std::unique_ptr<QThread> thread(QThread::create([] (QPromise<int> promise) {
promise.start();
promise.addResult(42);
promise.finish();
}, std::move(promise)));
thread->start();
future.waitForFinished();
future.result();
上記はマニュアルに記載のあるQThreadの実行結果をQFutureで待ち合わせて結果取得するだけですが、他にQFuture側からのキャンセル要求チェックやサスペンド、進捗通知など細かなやり取りが面倒な同期周りはおまかせで実装できるようになります。
なお@argama147さんも「Qt6の新機能 Promiseモードを使ってみる」という記事を上げているようです。そちらも見てみると良いのかもしれません。
QtConcurrent::task
QtConcurrentは、QThreadをはじめとする低レベルなAPIを使わなくても簡単にマルチスレッドを実現できるよう提供された高レベルAPIです。スレッドプールを使うことで事前にスレッドの数や準備をしておくことができ、少ないコードでスレッド処理を実装する機能群を提供しています。
中でもQtConcurrent::runは、その場で引き渡した関数をスレッドで動作させることができるため、短期間で終了する単純なスレッドを作成するのに非常に便利な機能でした。Qt4の頃から提供されていましたが、特にラムダ式が利用できるようになってから、非常に有効な機能です。
こちらに、Qt6.6から新しいAPIが1つ追加されていました。それがQtConcurrent::taskです。
QtConcurrent::QTaskBuilder<Task> QtConcurrent::task(Task &&task)
今までのQtConcurrentと違ってQFutureを返していないのは、このAPI呼び出し時点ではスレッドが起動しないからです。まぁ、使った例を見ないとピンと来ませんよね。
#include <QCoreApplication>
#include <QtConcurrent>
#include <QException>
#include <QList>
#include <QDebug>
#include <numeric>
int main(int argc, char* argv[])
{
QCoreApplication app(argc, argv);
auto task = QtConcurrent::task([](const QList<int>& list)->int{
if (list.empty()) return 0;
QThread::sleep(2);
return std::accumulate(list.begin(), list.end(), 0, [](int m, int n) {
return std::gcd(m, n);
});
});
auto f1 = task.withArguments(QList<int>{12,42,72}).spawn();
auto f2 = task.withArguments(QList<int>{18,98,128}).spawn();
QFutureSynchronizer<int> synchronizer;
synchronizer.setFuture(f1);
synchronizer.setFuture(f2);
synchronizer.waitForFinished();
qDebug() << f1.result() << f2.result();
}
ホントは複雑で時間のかかる例にすべきなのかもですが、何をやっているかわからなくなるので2秒後に渡したリストの最大公約数をもとめるようにしてみました。これを実行するとほぼ2秒後に"6 2"の数値が出力されます。つまり、2つのsleepが並列のスレッドで走った後最大公約数を求めて終了します。
勘が良い人は気がついたでしょうけど、これQtConcurrent::runが即時実行なのに対して、スレッド実行可能なタスクを用意し、後から条件付してスレッド実行させる機能です。
まぁ、関数用意して内部でQtConcurrent::runを呼び出すってやってたものをよりシンプルにかけるという感じでしょうか。
まとめ
本日は体調が微妙で投稿時間を大幅にすぎていたので、思い切り手抜きで、昔の記事からの最近までに追加されたスレッド関連機能を紹介してみました。
まぁ、QThreadでがりがり書くひとはQPromiseなくても色々やるクラス作っているでしょうし、QtConcurrent::runがあれば大半は足りますし、よほどでなければQRecursiveMutexは必要ないかもしれませんけど、新規に手をだして手抜きしたいときに使える機能があるって良いですよね。
Qtカレンダー明日はまだ空白予定です。1日遅れの夜中に記事を書いている状態で明日はSlintの記事も書く予定なので、Qt側穴埋めできるかはわかりませんが、Slint記事終えで時間に余裕があって、それでも空いてたらなにか書きます。
急に寒くなりましたし、風邪も、それ以外も流行っていそうなのでみなさんしっかりあたたかくして、体大事にしましょう。