Harrix Блог

  • Списки статей
    • Все статьи
    • IT
    • Qt
    • C++
    • Сложение двух чисел
    • Web программированиe
    • FAQ
    • Latex
    • Установка программ
    • Мифы
    • Видео
    • Про фото
  • Проекты
  • Harrix.org
  • RSS
  • Контакты

Как пользоваться QThread в Qt

В статье рассказывается о неправильном и более правильном способах использования механизма потоков через QThread в Qt.

20.02.2016 2 комментария 45 267 просмотров

В статье рассказывается о неправильном и более правильном способах использования механизма потоков через QThread в Qt.

Содержание

  • Неправильный способ
  • Более правильный способ

Исходники

Потоки — удобный механизм для работы с приложением, в котором есть долговыполняемые функции, и не хотелось бы, чтобы интерфейс приложения тормозил. Разумеется, что есть еще очень много ситуаций, когда выполнение функций рекомендуется производить не в основном потоке.

Неправильный способ

Самый простой в реализации способ — создать потомка от класса QThread и переопределить метод run. Но обратите внимание, данный способ не соответствует идеологии ООП и многие его не рекомендуют.

Создадим приложение, в котором при нажатии кнопки должна запуститься функция, которая увеличивает счетчик на 1 каждую секунду, а остальное время спит. При этом в textEdit выводится информация о состоянии счетчика.

1
2
3
4
for(int i = 0; i <= 100; i++)
{
Sleep(1000);
}

Внешний вид приложения будет таким:

2016-02-20_114346

Если реализовать без дополнительного потока, то наше приложение при нажатии на кнопку просто зависнет и не будет отликаться на наши действия.

Итак, создадим новый класс MyThread.

Один из способов создания класса в Qt
2016-02-20_122046
2016-02-20_122107
2016-02-20_122142
2016-02-20_122212

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

Что у нас тут есть?

  1. Класс наследуется от класса QThread, а также обладает макросом Q_OBJECT. Без него не будут работать сигналы и слоты.
  2. Конструктор потока теперь принимает параметры, например, имя потока. Таким образом в поток можно передать нужную информацию до его запуска.
  3. Основная деятельность потока реализована в методе run().
  4. У нас есть сигнал void send(int), который из потока отправляется при каждом изменении счетчика i в методе run():
  5. 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);

2016-02-20_131433

Не забудьте в 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();
}

Теперь при запуске нашей программы и нажатии на кнопку каждую секунду будет появляться значение счетчика:

2016-02-20_131724

Допустим, что вам нужно отправить некоторые данные из главного потока в новый поток, но не с помощью конструктора копии. Тогда вам нужно создать сигнал в главном потоке, а обрабатывающий его слот в новом потоке, то есть сделать всё наоборот, чем в примере выше.

В примере ниже мы добавим переменную 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);

2016-02-20_140136

Соединяем слот и сигнал (обратите внимание на объявление кто отправитель сигнала, а кто получатель), а потом можем сигнал отправлять:

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();
}

При запуске программы и нажатии на кнопку мы увидим, что сигнал из главного потока в новый поток прошел успешно:

2016-02-20_140539

Более правильный способ

По идеи класс 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, чтобы этот метод запустился при старте потока.

И потом поток запускаем.

2016-02-20_164417

Если же хотите отправить сигнал в поток из главного, то прописываем в классе 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.

2016-02-20_173623

Пока всё)


Статьи по теме:

  1. FAQ по Qt
  2. Распространенные ошибки в Qt у начинающих
  3. Сложение двух чисел в Qt 5.4.0 на C++ (консольное приложение)
  4. Qt Quick и C++ в Qt 5.6.0 Краткая инструкция

IT C++, Qt

© 2014 Harrix