В статье рассказывается о неправильном и более правильном способах использования механизма потоков через QThread в Qt.
Содержание
Потоки — удобный механизм для работы с приложением, в котором есть долговыполняемые функции, и не хотелось бы, чтобы интерфейс приложения тормозил. Разумеется, что есть еще очень много ситуаций, когда выполнение функций рекомендуется производить не в основном потоке.
Неправильный способ
Самый простой в реализации способ — создать потомка от класса QThread и переопределить метод run. Но обратите внимание, данный способ не соответствует идеологии ООП и многие его не рекомендуют.
Создадим приложение, в котором при нажатии кнопки должна запуститься функция, которая увеличивает счетчик на 1 каждую секунду, а остальное время спит. При этом в textEdit выводится информация о состоянии счетчика.
1 2 3 4 |
for(int i = 0; i <= 100; i++) { Sleep(1000); } |
Внешний вид приложения будет таким:
Если реализовать без дополнительного потока, то наше приложение при нажатии на кнопку просто зависнет и не будет отликаться на наши действия.
Итак, создадим новый класс MyThread.
mycalss.cpp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
#include "mythread.h" #include <QDebug> #include <windows.h> MyThread::MyThread(QString s) : name(s) { } void MyThread::run() { for(int i = 0; i <= 100; i++) { emit send(i); Sleep(1000); } } |
mycalss.h
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
#ifndef MYTHREAD_H #define MYTHREAD_H #include <QThread> #include <QString> class MyThread : public QThread { Q_OBJECT public: explicit MyThread(QString s); void run(); signals: void send(int); private: QString name; }; #endif // MYTHREAD_H |
Что у нас тут есть?
- Класс наследуется от класса QThread, а также обладает макросом Q_OBJECT. Без него не будут работать сигналы и слоты.
- Конструктор потока теперь принимает параметры, например, имя потока. Таким образом в поток можно передать нужную информацию до его запуска.
- Основная деятельность потока реализована в методе run().
- У нас есть сигнал void send(int), который из потока отправляется при каждом изменении счетчика i в методе run():
1 |
emit send(i); |
Для того, чтобы сигнал из потока получать в главном потоке, нам нужен слот, который будет данный сигнал принимать и обрабатывать. Поэтому в mainwindow.cpp пропишем слот:
1 2 3 4 |
void MainWindow::update (int i) { ui->textEdit->insertPlainText(QString::number(i)); } |
А в mainwindow.h объявим его:
1 |
void update (int i); |
Не забудьте в mainwindow.cpp подключить инклуд:
1 |
#include "mythread.h" |
Теперь в слоте нажатия на кнопку мы можем создать экземпляр нашего слота, соединить слот update и сигнал send, а потом уже запустить наш поток.
1 2 3 4 5 6 7 8 |
void MainWindow::on_pushButton_clicked() { MyThread *thread= new MyThread("A"); connect(thread, SIGNAL(send(int)), this, SLOT(update(int))); thread->start(); } |
Теперь при запуске нашей программы и нажатии на кнопку каждую секунду будет появляться значение счетчика:
Допустим, что вам нужно отправить некоторые данные из главного потока в новый поток, но не с помощью конструктора копии. Тогда вам нужно создать сигнал в главном потоке, а обрабатывающий его слот в новом потоке, то есть сделать всё наоборот, чем в примере выше.
В примере ниже мы добавим переменную int globalVar в класс, которую и поменяем из главного потока.
Обрабатывать сигнал будем слотом receiveGlobalVar(int globalVar).
Модифицированные файлы:
mycalss.cpp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
#include "mythread.h" #include <QDebug> #include <windows.h> MyThread::MyThread(QString s) : name(s) { } void MyThread::run() { for(int i = 0; i <= 100; i++) { emit send(i); Sleep(1000); } } void MyThread::receiveGlobalVar(int globalVar) { qDebug() << "globalVar = " << globalVar; this->globalVar = globalVar; } |
mycalss.h
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
#ifndef MYTHREAD_H #define MYTHREAD_H #include <QThread> #include <QString> class MyThread : public QThread { Q_OBJECT public: explicit MyThread(QString s); void run(); public slots: void receiveGlobalVar (int i); signals: void send(int); private: QString name; int globalVar; }; #endif // MYTHREAD_H |
И в mainwindow.h добавим сигнал:
1 2 |
signals: void sendGlobalVar(int); |
Соединяем слот и сигнал (обратите внимание на объявление кто отправитель сигнала, а кто получатель), а потом можем сигнал отправлять:
1 |
connect(this, SIGNAL(sendGlobalVar(int)), thread, SLOT(receiveGlobalVar(int))); |
1 |
emit sendGlobalVar(7); |
Вот полный код нажатия на кнопку:
1 2 3 4 5 6 7 8 9 10 11 |
void MainWindow::on_pushButton_clicked() { MyThread *thread= new MyThread("A"); connect(thread, SIGNAL(send(int)), this, SLOT(update(int))); connect(this, SIGNAL(sendGlobalVar(int)), thread, SLOT(receiveGlobalVar(int))); emit sendGlobalVar(7); thread->start(); } |
При запуске программы и нажатии на кнопку мы увидим, что сигнал из главного потока в новый поток прошел успешно:
Более правильный способ
По идеи класс QThread не предназначен для прописывания функционала, который должен выполняться в новом потоке. Его предназначение создавать новый поток и управлять тем, что в нем происходит.
К тому же, как в примере выше, нам пришлось добавлять переменные в класс (globalVar), чтобы можно было получить к ним доступ из разных методов и слотов данного класса. Это всё не очень красиво и не соответствует духу ООП.
Куда правильнее будет прописать класс с нужным функционалом, создать его экземпляр и запихать его в экземпляр нового потока QThread. Что мы и сделаем.
Вначале создадим класс MyClass.
myclass.cpp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
#include "myclass.h" #include <QDebug> #include <windows.h> MyClass::MyClass(QString s) : name(s) { } void MyClass::doWork() { for(int i = 0; i <= 100; i++) { emit send(i); Sleep(1000); } } |
myclass.h
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
#ifndef MYCLASS_H #define MYCLASS_H #include <QObject> #include <QString> class MyClass:public QObject { Q_OBJECT public: MyClass(QString name); public slots: void doWork(); signals: void send(int); private: QString name; }; #endif // MYCLASS_H |
Здесь теперь наш класс будет обладать макросом Q_OBJECT, а также заниматься отправкой сигналов и приемом предназначенных ему сигналов.
Основной функционал будет осуществляться в методе doWork(), где также как и «неправильном» методе отправляется сигнал send().
В mainwindow.cpp мы уже не будем прописывать слот для обработки сигнала update, так как уже он написан для «неправильного» способа.
Так что подключим только соответствующий инклуд:
1 |
#include "myclass.h" |
И в слоте нажатия второй кнопки пропишем следующий код:
1 2 3 4 5 6 7 8 9 |
QThread *thread= new QThread; MyClass *my = new MyClass("B"); my->moveToThread(thread); connect(my, SIGNAL(send(int)), this, SLOT(update(int))); connect(thread, SIGNAL(started()), my, SLOT(doWork())); thread->start(); |
Вначале мы создали экземпляр класса QThread для нового потока в приложении.
Создали экземпляр нашего класса.
Потом переместили экземпляр нашего класса в новый поток.
Соединили сигнал из экземпляра класса MyClass и принимающего слота из главного потока.
Соединили сигнал нового потока о старте работы потока с методом doWork() класса MyClass, чтобы этот метод запустился при старте потока.
И потом поток запускаем.
Если же хотите отправить сигнал в поток из главного, то прописываем в классе MyClass соответствующий слот для принятия сигнала receiveGlobalVar():
myclass.cpp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
#include "myclass.h" #include <QDebug> #include <windows.h> MyClass::MyClass(QString s) : name(s) { } void MyClass::doWork() { for(int i = 0; i <= 100; i++) { emit send(i); Sleep(1000); } } void MyClass::receiveGlobalVar(int globalVar) { qDebug() << "globalVar = " << globalVar; this->globalVar = globalVar; } |
myclass.h
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
#ifndef MYCLASS_H #define MYCLASS_H #include <QObject> #include <QString> class MyClass:public QObject { Q_OBJECT public: MyClass(QString name); public slots: void doWork(); void receiveGlobalVar (int i); signals: void send(int); private: QString name; int globalVar; }; #endif // MYCLASS_H |
on_pushButton_2_clicked()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
QThread *thread= new QThread; MyClass *my = new MyClass("B"); my->moveToThread(thread); connect(my, SIGNAL(send(int)), this, SLOT(update(int))); connect(thread, SIGNAL(started()), my, SLOT(doWork())); connect(this, SIGNAL(sendGlobalVar(int)), my, SLOT(receiveGlobalVar(int)), Qt::DirectConnection); emit sendGlobalVar(7); thread->start(); emit sendGlobalVar(7); |
Обратите внимание, что теперь связывать слот и сигнал желательно с параметром Qt::DirectConnection.
Пока всё)