Реклама

Qt/C++ - Урок 048. QThread - работа с потоками с помощью moveToThread

moveToThread, QObject, Qt, QThread, потоки

В предыдущей статье мы только коснулись работы с потоками , причём в том варианте, который служит больше для кастомизации самого потока, хотя и может быть использован для совершения посторонней работы, никак не связанной непосредственно с потоками. То есть вариант с наследованием от QThread и переопределение метода run() .

Теперь же мы создадим класс ExampleObject, объекты которого будут передаваться в отдельные потоки с помощью метода moveToThread() и выполняться в этих самых потоках. А полезную работу будет совершать слот-метод run(), который будет определён в данном классе. Что важно, класс объектов будет наследован от QObject .

Для того, чтобы работа в методе run() могла выполняться циклично, используем цикл while , управлять которым будем с помощью переменной bool m_running. Для удобства работы с этой переменной определяем её в качестве Q_PROPERTY. Ну а по завершении работы будем испускать сигнал finished() .

Алгоритм работы

А теперь обратимся непосредственно к работе с потоком и объектом. Алгоритм работы будет следующий:

  1. Создаём объект QThread и объект класса ExampleObject;
  2. Подключаем сигнал QThread::started() к методу ExampleObject::run();
  3. Подключаем сигнал ExampleObject::finished() к слоту QThread::terminate(), чтобы по завершении выполнения полезной работы завершить выполнение потока;
  4. Устанавливаем переменную m_running в значение true, чтобы разрешить работу цикла, иначе метод сразу завершится;
  5. Запускаем поток с помощью метода start();
  6. Когда нужно завершить выполнение полезной работы объекта, устанавливаем переменную m_running в значение false. Выполнение метода run() и потока в котором живёт объект завершатся автоматически и корректно.

Структура проекта и внешний вид приложения

  • ThreadLessonTwo.pro - профайл проекта;
  • exampleobject.h - заголовочный файл объекта, который будет передаваться в поток;
  • exampleobject.cpp - файл исходных кодов объекта, который будет передаваться в поток;
  • mainwindow.h - заголовочный файл главного окна приложения;
  • mainwindow.cpp - файл исходных кодов главного окна приложения;
  • mainwindow.ui - файл формы главного окна приложения;
  • main.cpp - файл исходных кодов с функцией main.

В приложении будет определено два объекта и два потока. С помощью интерфейса приложения мы будем задавать некоторую отображаемую в выводе qDebug() информацию, а также будем запускать и останавливать работу потоков.

Процесс создания окна приложения показан в видеоуроке.

exampleobject.h

Для начала рассмотрим содержимое заголовочного файла объекта. В данном файле объявлено три свойства Q_PROPERTY :

  1. running - это переменная, с помощью которой будет осуществляться управление циклом в методе run() и соответственно будет влиять на завершение выполнения полезной работы объекта.
  2. message - это строка, которая будет передаваться для вывода в qDebug() из главного окна приложения
  3. message_2 - это вторая строка, которая также будет отображаться в qDebug(), но при этом будет использоваться и для передачи во второй поток.

Также в заголовочном файле объявлены слот-метод run(), сигнал finished() и переменная int count, которая

#ifndef EXAMPLEOBJECT_H
#define EXAMPLEOBJECT_H

#include <QObject>

class ExampleObject : public QObject
{
    Q_OBJECT
    // Свойство, управляющее работой потока
    Q_PROPERTY(bool running READ running WRITE setRunning NOTIFY runningChanged)
    // Первое сообщение в объекте
    Q_PROPERTY(QString message READ message WRITE setMessage NOTIFY messageChanged)
    // Второе сообщение, которое будем передавать через сигнал/слот во второй объект
    Q_PROPERTY(QString message_2 READ message_2 WRITE setMessage_2 NOTIFY message_2Changed)

    bool m_running;
    QString m_message;
    QString m_message_2;
    int count;  // Счётчик, по которому будем ориентироваться на то,
                // что потоки выполняются и работают

public:
    explicit ExampleObject(QObject *parent = 0);
    bool running() const;
    QString message() const;
    QString message_2() const;

signals:
    void finished();    // Сигнал, по которому будем завершать поток, после завершения метода run
    void runningChanged(bool running);
    void messageChanged(QString message);
    void message_2Changed(QString message_2);
    void sendMessage(QString message);

public slots:
    void run(); // Метод с полезной нагрузкой, который может выполняться в цикле
    void setRunning(bool running);
    void setMessage(QString message);
    void setMessage_2(QString message_2);
};

#endif // EXAMPLEOBJECT_H

exampleobject.cpp

Весь наш интерес сводится к методу run(), в котором в цикле while будет инкрементироваться счётчик count и будет выводиться информация с сообщением m_message, m_message_2 и данным счётчиком до тех пор, пока переменная m_running не будет выставлена в значение false . По выходу из цикла при завершении выполнения метода run() будет выпущен сигнал finished() , по которому завершится поток, в котором будет находиться данный объект.

#include "exampleobject.h"
#include <QDebug>

ExampleObject::ExampleObject(QObject *parent) :
    QObject(parent),
    m_message(""),
    m_message_2("")
{

}

bool ExampleObject::running() const
{
    return m_running;
}

QString ExampleObject::message() const
{
    return m_message;
}

QString ExampleObject::message_2() const
{
    return m_message_2;
}

// Самый важный метод, в котором будет выполняться "полезная" работа объекта
void ExampleObject::run()
{
    count = 0;
    // Переменная m_running отвечает за работу объекта в потоке.
    // При значении false работа завершается
    while (m_running)
    {
        count++;
        emit sendMessage(m_message); // Высылаем данные, которые будут передаваться в другой поток
        qDebug() << m_message << " " << m_message_2 << " " << count;
    }
    emit finished();
}

void ExampleObject::setRunning(bool running)
{
    if (m_running == running)
        return;

    m_running = running;
    emit runningChanged(running);
}

void ExampleObject::setMessage(QString message)
{
    if (m_message == message)
        return;

    m_message = message;
    emit messageChanged(message);
}

void ExampleObject::setMessage_2(QString message_2)
{
    if (m_message_2 == message_2)
        return;

    m_message_2 = message_2;
    emit message_2Changed(message_2);
}

mainwindow.h

В заголовочном файле главного окна приложения объявлены слоты для обработки нажатий кнопок для записи сообщений в объекты, которые будут выполняться в потоках, а также слоты для запуска и остановки потоков. Помимо этого объявим два объекта класса ExampleObject и два объекта класса QThread.

#ifndef MAINWINDOW_H
#define MAINWINDOW_H

#include <QMainWindow>
#include <QThread>
#include "exampleobject.h"

namespace Ui {
class MainWindow;
}

class MainWindow : public QMainWindow
{
    Q_OBJECT

public:
    explicit MainWindow(QWidget *parent = 0);
    ~MainWindow();

private slots:
    void on_write_1_clicked();  // Слот для записи данных из lineEdit_1 в первый объект первого потока
    void on_write_2_clicked();  // Слот для записи данных из lineEdit_1 во второй объект второго потока
    void on_start_clicked();    // Слот для запуска потоков
    void on_stop_clicked();     // Слот для остановки потоков

private:
    Ui::MainWindow *ui;
    QThread thread_1;       // Первый поток
    QThread thread_2;       // Второй поток
    ExampleObject exampleObject_1;  // первый объект, который будет работать в первом потоке
    ExampleObject exampleObject_2;  // второй объект, который будет работать во втором потоке
};

#endif // MAINWINDOW_H

mainwindow.cpp

А теперь объединим весь предыдущий код в рабочее приложение в исходном коде главного окна приложения. В конструкторе данного класса, как уже было сказано в самом начале, необходимо подключить сигналы started() потоков к слотам run() наших тестовых объектов, а сигналы finished(), к слотам потоков terminate(), чтобы завершать работу потоков при завершении выполнения метода run().

Также присоединим сигнал посылки сообщения объекта exampleObject_1 к слоту установки сообщения exampleObject_2. Но чтобы информация могла передаваться, нужно пятым аргументом в метод connect передать флаг Qt::DirectConnection , который установит непосредственное соединение объектов и позволит выполнять передачу информации через систему сигналов и слотов .

Ну и для передачи объектов в потоки необходимо воспользоваться методом moveToTrhead().

#include "mainwindow.h"
#include "ui_mainwindow.h"

MainWindow::MainWindow(QWidget *parent) :
    QMainWindow(parent),
    ui(new Ui::MainWindow)
{
    ui->setupUi(this);

    // Запуск выполнения метода run будет осуществляться по сигналу запуска от соответствующего потока
    connect(&thread_1, &QThread::started, &exampleObject_1, &ExampleObject::run);
    connect(&thread_2, &QThread::started, &exampleObject_2, &ExampleObject::run);
    // Остановка потока же будет выполняться по сигналу finished от соответствующего объекта в потоке
    connect(&exampleObject_1, &ExampleObject::finished, &thread_1, &QThread::terminate);
    connect(&exampleObject_2, &ExampleObject::finished, &thread_2, &QThread::terminate);
    // коннект для передачи данных из первого объекта в первом потоке, ко второму объекту во втором потоке
    connect(&exampleObject_1, &ExampleObject::sendMessage, &exampleObject_2, &ExampleObject::setMessage_2, Qt::DirectConnection);
    exampleObject_1.moveToThread(&thread_1);    // Передаём объекты в соответствующие потоки
    exampleObject_2.moveToThread(&thread_2);
}

MainWindow::~MainWindow()
{
    delete ui;
}

void MainWindow::on_write_1_clicked()
{
    // Устанавливаем текст в первый объект в первом потоке
    exampleObject_1.setMessage(ui->lineEdit_1->text());
}

void MainWindow::on_write_2_clicked()
{
    // Устанавливаем текст во второй объект во втором потоке
    exampleObject_2.setMessage(ui->lineEdit_2->text());
}

void MainWindow::on_start_clicked()
{
    // Запуск потоков
    exampleObject_1.setRunning(true);
    exampleObject_2.setRunning(true);
    thread_1.start();
    thread_2.start();
}

void MainWindow::on_stop_clicked()
{
    // Остановка потоков через завершение выполнения методов run в объектах
    exampleObject_1.setRunning(false);
    exampleObject_2.setRunning(false);
}

Вывод

В результате работы приложения можно получить ниже следующий вывод qDebug() , который демонстрирует параллельную работу объектов в потоках, а также передачу информации из одного потока в другой.

"thread 1"   ""   11422
"thread 2"   "thread 1"   11446
"thread 1"   ""   11423
"thread 2"   "thread 1"   11447
"thread 1"   ""   11424
"thread 2"   "thread 1"   11448
"thread 1"   ""   11425
"thread 2"   "thread 1"   11449
"thread 1"   ""   11426
"thread 2"   "thread 1"   11450
"thread 1"   ""   11427
"thread 2"   "thread 1"   11451

Скачать проект можно по следующей ссылке: Thread Lesson Two

Видеоурок

Реклама

Комментарии

У меня происходит переполнение счетчика count, появляется ошибка malloc(): memory corruption (fast). Не подскажите, как с этим бороться?

А что делали? Повторяете урок или как? Пытались просто скачать проект в конце статьи и запустить?

  • #
  • 7 августа 2017 г. 9:12

Взял Ваш пример для Qt4.8 (не спрашивайте почему). переписал связь сигнал/слот след. образом:

    // Запуск выполнения метода run будет осуществляться по сигналу запуска от соответствующего потока
    QObject::connect(&thread_1,        SIGNAL(started()),
                     &exampleObject_1, SLOT(run()));
    QObject::connect(&thread_2,        SIGNAL(started()),
                     &exampleObject_2, SLOT(run()));
    //connect(&thread_1, &QThread::started, &exampleObject_1, &ExampleObject::run);
    //connect(&thread_2, &QThread::started, &exampleObject_2, &ExampleObject::run);
    // Остановка потока же будет выполняться по сигналу finished от соответствующего объекта в потоке

    QObject::connect(&exampleObject_1,        SIGNAL(finished()),
                     &thread_1, SLOT(terminate()));
    QObject::connect(&exampleObject_2,        SIGNAL(finished()),
                     &thread_2, SLOT(terminate()));
    //connect(&exampleObject_1, &ExampleObject::finished, &thread_1, &QThread::terminate);
    //connect(&exampleObject_2, &ExampleObject::finished, &thread_2, &QThread::terminate);
    // коннект для передачи данных из первого объекта в первом потоке, ко второму объекту во втором потоке
    QObject::connect(&exampleObject_1,        SIGNAL(sendMessage(QString)),
                     &exampleObject_2,        SLOT(setMessage_2(QString)));
    //connect(&exampleObject_1, &ExampleObject::sendMessage,
    //        &exampleObject_2, &ExampleObject::setMessage_2, Qt::DirectConnection);

В итоге получаю

"thread 1" "" 1752

"thread 2" "" 1747

"thread 1" "" 1753

"thread 2" "" 1748

"thread 1" "" 1754

т.е. связь между потоками не работает.


Вижу еще волшебное слово Qt::DirectConnection, пробовал добавить - нет результата

Добавил  еще в pro файл запись CONFIG += no_keywords - тоже не помогло

Что подскажите


PS: я нубер в Qt и плюсах

Не вижу всего кода, по этому куску кода невозможно сказать, в чём ошибка. На первый взгляд код написан верно.

P/S/ Если соберетёсь кидать весь код, то создавайте тогда новую тему на форуме. Там есть возможность прикрепить архив с проектом к теме и сообщениям. В комментариях под статьёй не стоит постить большие куски кода.

Форум здесь .

В остальном код как и Вашем примере

Тогда мне больше нечего Вам сказать. Давно уже не работал с Qt 4.8 и приниципиально не работаю с ним сейчас.

  • #
  • 15 августа 2017 г. 13:49

В qt5.6 всё нормально заработало. С 4.8 - нет

Нууу... тут уже вопрос к самому Qt4.8. Если честно, идей нет, да и копаться в deprecated коде желания тоже нет.

  • #
  • 4 сентября 2017 г. 14:57

В моем коде с двумя потоками и выводом сообщений в textEdit при использовании Qt::DirectConnection падает в "The program has unexpectedly finished" через 10-20 сообщений.  При использовании Qt::QueuedConnection, работает.

Все же как правильно использовать ?
Нашел на другом форуме:
  • Qt::DirectConnection — сигнал обрабатывается сразу вызовом соответствующего метода слота
  • Qt::QueuedConnection — сигнал преобразуется в событие и ставится в общую очередь для обработки
  • Qt::AutoConnection — это автоматический режим, который действует следующим образом: если отсылающий сигнал объект находится в одном потоке с принимающим его объектом, то устанавливается режим Qt::DirectConnection , в противном случае — режим Qt::QueuedConnection . Этот режим ( Qt::AutoConnection ) определен в методе connection() по умолчанию.

В принципе все варианты возможны, зависит от ситуации.
Наиболее правильным по максимому использовать Qt::AutoConnection. Если что-то по какой-то причине не работает, то разбираться в чём причина и возможно переходить на Qt::DirectConnection , при данном методе слот будет вызываться сразу же, юез постановки в очередь событий. Иногда может потребоваться, чтобы дать приоритет вызову слота. Ведь слоты вызываются в той последовательности, как они был подключены в коде.
Тут уже возникает вопрос в потокобезопасности. Поэтому сложный код следует блокировать мьютексами.Теми же самыми QMutexLocker и QMutex, например,

int complexFunction(int flag)
{
    QMutexLocker locker(&mutex);

    // ToDo something
    return retVal;
}
В данном случае mutex является объектов класса QMutex, который объявлен в заголовке класса. Можете проверить у себя работу вашего класса с использование QMutex и Qt::DirectConnection .

В том и дело что хотел без мютексов обойтись. т.е. суть проблемы есть старая программа, рабочая и три потока в ней  A-B-(C1...Cn) .

A - поток основной программы;
B - поток для QModbusTcpServer;
С1...Cn - потоки опроса приборов по протоколу xxx [RTU];

Задача передать из потоков собранные данные потоков C1...Cn в один JSON контейнер и отправить в QWebSocketServer.
В QWebSocketServer создал слот (для отладки и в MainWindow) привязал сигналы из C1...Cn,  передаю простой тип QString.

Вопрос , на ваш взгляд потокобезопасно так делать или нет ? Ну и как писал Qt::DirectConnection вызывает глюк, без него работает.
P.S. Qt 5.6.1

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

  • Qt::QueuedConnection является потокобезопасным вариантом и информация не портится при передаче данных потому, что все сигналы отправляют информацию в очередь сообщений и информация обрабатывается поочерёдно. Это актуально для передачи информации между несколькими потоками.
  • Qt::DirectConnection используется тогда, когда и отправитель сигнала и получатель находятся в одном потоке, поскольку такая передача информации будет потокобезопасной. Всё равно другой код не начнёт работу с текущим слотом, пока слот не завершится. Но это не значит, что такое соединение нельзя использовать при передачи информации между потоками. Только в данном случае нужно обезопаситься мьютексами. Тем более, что их реализация позволяет это делать в одну строку в методе.
Поэтому у вас всё заработало с Qt::QueuedConnection и сломалось с Qt::DirectConnection, очевидно потоки конкурировали друг с другом и получилось, что была испорчена память.

Поэтому в вашем случае работайте без Qt::DirectConnection , и подумайте над использованием мьютексов. Если начнёт падать и с Qt::QueuedConnection , то стоит подумать над подключением мьютексов.

Комментарии

Только авторизованные пользователи могут оставлять комментарии.
Пожалуйста, Авторизуйтесь или Зарегистрируйтесь

Qt - Тест 001. Сигналы и слоты

  • Результат 0 баллов
  • Очки рейтинга -10
  • boa
  • 10 декабря 2017 г. 3:04

Qt - Тест 001. Сигналы и слоты

  • Результат 84 баллов
  • Очки рейтинга 4
  • Shalfy
  • 8 декабря 2017 г. 14:05

Qt - Тест 001. Сигналы и слоты

  • Результат 100 баллов
  • Очки рейтинга 10
Последние комментарии
  • EVILEG
  • 7 декабря 2017 г. 9:47

Django - Урок 011. Добавление комментариев на сайт с Django

Визуальный пример чего? комментариев? При ответе на конкретный комментарий рядом с ником отвечающего будет стрелочка и указание ник другого пользователя. Который будет ссылкой на коммента...

  • Bernar
  • 7 декабря 2017 г. 9:24

Django - Урок 011. Добавление комментариев на сайт с Django

есть визуальный пример ?

  • EVILEG
  • 6 декабря 2017 г. 11:30

Django - Урок 011. Добавление комментариев на сайт с Django

Да, так будет даже лучше, я на сайте уже обновил до такого вида код Вот это уже не нужно if request.method == 'POST': Поскольку Вы и так используете метод post, то есть эта про...

  • Bernar
  • 6 декабря 2017 г. 11:19

Django - Урок 011. Добавление комментариев на сайт с Django

сделал немного по другому class EArticleView(View): template_name = 'knowledge/article.html' comment_form = CommentForm def get(self, request, *args, **kwargs): ...

Сейчас обсуждают на форуме

Как значение текущего элемента ListView получить вне ListView

Вот реально огромное спасибо!! У вас большое терпение с нами возиться и отзывчивость.

  • EVILEG
  • 11 декабря 2017 г. 8:09

QCustomPlot исчезает часть графика при перестроении

Объявить volumeAxisRect в заголовочном файле

  • EVILEG
  • 9 декабря 2017 г. 21:24

Как написать парсер страницы при помощи js

Эм... лично я даже растерялся от такого вопроса... У javascript есть методы типо document.getElementById document.getElementByTag, которые выбирают нужные теги и мож...

Ошибки при многопоточном запуске функции библиотке

Большое спасибо, так заработало QFuture<void> Perebor2 = QtConcurrent::map(Perebor,[=](const double& d){ StrategyCod(d,this);});

  • grig_p
  • 8 декабря 2017 г. 12:49

Прерывание таймера в режиме singleShot

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