C++(に限らないけど)のプログラミングで難しいのは、メモリ管理と並行管理です。
Qtを使うと、これらのかなりの部分が簡単になります。が、何が起きているのかを理解していないと、正しく使うことができないので、今回はsignal/slotとthreadについて例を挙げて説明してみようと思います。(メモリ管理については軽く触りますが、また別の機会に)
サンプルは、githubに上げてあります。
サンプルをビルドして実行すると、ボタンが3つあるウィンドウが表示されます。それぞれのボタンを押すことで、signal/slotにより処理が実行されます。
ソース
まずは、簡単にソースの説明。抜粋していきますので、フルのソースはgithubを見てください。
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
qDebug() << QThread::currentThreadId() << "QApplication::exec()を実行するスレッド";
MainWindow w;
w.show();
return a.exec();
}
Qt Creatorが作ったひな形のままですが、qDebug()で実行しているスレッドをデバッグ出力しています。
QThread::currentThreadId()
は、現在のコードを実行しているスレッドのIDです。pthreadを使っているシステムでは、そのままpthread_self()
と同じ内容になります。
MainWindow::MainWindow(QWidget *parent) :
QMainWindow(parent)
{
// 画面部品を作る
QWidget *widget = new QWidget;
QVBoxLayout *layout = new QVBoxLayout;
QPushButton *button1 = new QPushButton("button 1");
layout->addWidget(button1);
QPushButton *button2 = new QPushButton("button 2");
layout->addWidget(button2);
QPushButton *button3 = new QPushButton("button 3");
layout->addWidget(button3);
widget->setLayout(layout);
setCentralWidget(widget);
// シグナルとスロットを繋ぐ
connect(button1, &QPushButton::clicked, this, &MainWindow::button1Clicked);
connect(button2, &QPushButton::clicked, this, &MainWindow::button2Clicked);
connect(button3, &QPushButton::clicked, this, &MainWindow::button3Clicked);
connect(this, &MainWindow::someSignal1, this, &MainWindow::someSlot1); // Qt::AutoConnection
connect(this, &MainWindow::someSignal2, this, &MainWindow::someSlot2, Qt::QueuedConnection);
}
画面の部品を並べて、signalとslotをconnectしています。
ソースを短くしたかったので、uiファイル等は使っていません。
ここで気にしておくのは、new した QWidget や QPushButtonの寿命です。
これらのQObjectのサブクラスは、全て内部にparentと言うプロパティを持っており、parentが削除されるときには、自動的に子も削除されます。
parentはコンストラクタの引数で指定することができますが、今回のように画面部品の場合は、コンストラクタで指定しなくても、親部品に追加したときに自動的に親が設定されます。
例えば、MainWindowにwidgetを追加するところの前後で、parentを表示させてみると、
qDebug() << widget->parent();
setCentralWidget(widget);
qDebug() << widget->parent();
QObject(0x0)
MainWindow(0x7fff5f8cab68)
のように表示され、setCentralWidget()の呼び出しにより、widgetに親が設定されることがわかります。
動作
button 1
void MainWindow::button1Clicked()
{
qDebug() << QThread::currentThreadId() << "button 1が押されたときに呼ばれるslotのスレッド";
emit someSignal1();
qDebug() << QThread::currentThreadId() << "emit完了!";
}
void MainWindow::someSlot1()
{
qDebug() << QThread::currentThreadId() << "someSlot1が呼ばれたときのスレッド";
}
button 1を押すと、QPushButtonのclickedにconnectされたslotであるbutton1Clicked()が呼ばれます。
button1Clicked()は、someSignal1()と言うsignalをemitします。
someSignal1は、someSlot1とconnectされているので、someSlot1が呼ばれます。
このときのデバッグ出力は、以下のようになります。
0x7fff76cf3310 button 1が押されたときに呼ばれるslotのスレッド
0x7fff76cf3310 someSlot1が呼ばれたときのスレッド
0x7fff76cf3310 emit完了!
ここからわかることは、全て同じスレッドで実行され、emit の実行により、someSlot1が呼ばれ、someSlot1の実行が終わった後でemitの後に制御が戻ると言うことです。
このケースでは、signal/slotの関係は単なる関数呼び出しと同じです。
button 2
void MainWindow::button2Clicked()
{
qDebug() << QThread::currentThreadId() << "button 2が押されたときに呼ばれるslotのスレッド";
emit someSignal2();
qDebug() << QThread::currentThreadId() << "emit完了!";
}
void MainWindow::someSlot2()
{
qDebug() << QThread::currentThreadId() << "someSlot2が呼ばれたときのスレッド";
}
この部分のソースは、button 1 のときとまったく同じです。
では、button 2を押してみましょう。
0x7fff76cf3310 button 2が押されたときに呼ばれるslotのスレッド
0x7fff76cf3310 emit完了!
0x7fff76cf3310 someSlot2が呼ばれたときのスレッド
違いがわかるでしょうか?
今回も、全て同じスレッドで実行されていますが、emitの後に制御が移った後(button2Clicked()の実行が終わった後)で、someSlot2()に制御が移っています。
この違いは、MainWindowのコンストラクタでのconnect()の引数の違いにあります。
someSingal2/someSlot2のconnectでは、最後の引数にQt::QueuedConnection
が指定されています。
このときは、emitはその場でslotを呼び出さずに、QtのEventLoopが一回回った後で、slotを呼び出すのです。(これが嬉しいケースが思いつかないうちは、わざわざ使うことはないですよ)
button 3
Qtは、基本的にはシングルスレッドのイベントループで全てを処理しようとします。
このモデルの基本は、「全ての処理は速やかに終了する」です。
例えば、someSlot2()の実行に時間がかかったりすると、その間は画面が更新されず、ユーザの操作にも応答しないように見えてしまいます。
では、どうしても時間がかかる処理を実行したいときにはどうするかと言うと、スレッドを使います。
void MainWindow::button3Clicked()
{
qDebug() << QThread::currentThreadId() << "button 3が押されたときに呼ばれるslotのスレッド";
MyThread *t = new MyThread;
connect(t, &MyThread::finishCalculation, this, &MainWindow::someSlot3); // Qt::AutoConnection
connect(t, &MyThread::finished, t, &MyThread::deleteLater);
t->start();
}
void MainWindow::someSlot3()
{
qDebug() << QThread::currentThreadId() << "someSlot3が呼ばれたときのスレッド";
}
button 3が押されると、MyThreadを作って、start()します。
MyThreadのソースはこの後に載せますが、QThreadのサブクラスです。
MyThreadは、ボタンが押される度に立ち上がるので、その寿命はMainWindowとは同期しません。
このため、Qtのメモリ管理に任せるわけにはいかないので、不要になったタイミングでdeleteする必要があります。
このケースでは、QThreadのfinishedシグナルと、QObjectのdeleteLaterスロットをconnectすることで、スレッドの実行が終わったら削除されるようにしています。
class MyThread: public QThread {
Q_OBJECT
public:
MyThread() {
}
~MyThread() {
qDebug() << QThread::currentThreadId() << "~MyThread";
}
protected:
void run() {
qDebug() << QThread::currentThreadId() << "サブスレッド";
emit finishCalculation();
qDebug() << QThread::currentThreadId() << "emit完了!";
}
signals:
void finishCalculation();
};
スレッドのソースはこんな感じです。
ここでは、すぐにsingalをemitして終了してしまいますが、実際にはここで時間がかかる処理をすることになります。
プログレスバーを出すような場合は、一定の処理が進む毎に進捗を知らせるsignalがあると良いでしょう。
では、button 3を押してみます。
0x7fff76cf3310 button 3が押されたときに呼ばれるslotのスレッド
0x107403000 サブスレッド
0x107403000 emit完了!
0x7fff76cf3310 someSlot3が呼ばれたときのスレッド
0x7fff76cf3310 ~MyThread
button3Clicked()と、someSlot3()は、今までと同じメインスレッドで呼び出されていることがわかります。
そして、新しく作ったMyThreadのrunは、別のスレッドで実行されます。
deleteLatorの呼び出しは、メインスレッドです。
ここまで、スレッドを使っていても、全くmutex等の話が出てきていませんが、スレッド間のデータの受け渡しを、signal/slotでやる分には特に気にする必要はありません。(と言うか、ここまでのサンプルではデータの受け渡し自体をしていない)
では、データの受け渡しをする場合にはどのようになるのか?ですが、少し長くなってきたので次回に回したいと思います。
こんな記事をながなが読まなくても、マニュアルのThreads and QObjectsに全てが書いてありますので、そちらも読んでみてください。