У попередній статті ми тільки торкнулися роботи з потоками , причому в тому варіанті, який служить більше для кастомізації самого потоку, хоча і може бути використаний для здійснення сторонньої роботи , не пов'язаної безпосередньо з потоками. Тобто варіант із успадкуванням від QThread та перевизначення методу run() .
Тепер ми створимо клас ExampleObject, об'єкти якого будуть передаватися в окремі потоки за допомогою методу moveToThread() і виконуватись у цих самих потоках. А корисну роботу здійснюватиме слот-метод run(), який буде визначений у даному класі. Що важливо, клас об'єктів буде успадкований від QObject .
Для того, щоб робота в методі run() могла виконуватися циклічно, використовуємо цикл while , керувати яким будемо за допомогою змінної bool m_running. Для зручності роботи з цією змінною визначаємо її як * Q_PROPERTY. Ну а після завершення роботи будемо випускати сигнал finished() * .
Алгоритм роботи
А тепер звернемося безпосередньо до роботи з потоком та об'єктом. Алгоритм роботи буде наступним:
- Створюємо об'єкт QThread та об'єкт класу ExampleObject;
- Підключаємо сигнал QThread::started() до методу ExampleObject::run();
- Підключаємо сигнал ExampleObject::finished() до слота QThread::terminate(), щоб після завершення виконання корисної роботи завершити виконання потоку;
- Встановлюємо змінну m_running у значення true, щоб дозволити роботу циклу, інакше метод одразу завершиться;
- Запускаємо потік за допомогою методу start();
- Коли потрібно завершити виконання корисної роботи об'єкта, встановлюємо змінну m_running значення false. Виконання методу run() та потоку в якому живе об'єкт завершаться автоматично та коректно.
Структура проекту та зовнішній вигляд програми
- ThreadLessonTwo.pro - профайл проекту;
- exampleobject.h - заголовний файл об'єкта, який передаватиметься в потік;
- exampleobject.cpp - файл вихідних кодів об'єкта, який передаватиметься в потік;
- mainwindow.h - заголовний файл головного вікна програми;
- mainwindow.cpp - файл вихідних кодів головного вікна програми;
- mainwindow.ui - файл форми головного вікна програми;
- main.cpp - файл вихідних кодів із функцією main.
У програмі буде визначено два об'єкти та два потоки. За допомогою інтерфейсу програми ми будемо задавати деяку інформацію, що відображається у виведенні qDebug() , а також запускати і зупиняти роботу потоків.
Процес створення вікна програми показано у відеоуроці.
exampleobject.h
Для початку розглянемо вміст файлу заголовка об'єкта. У цьому файлі оголошено три властивості Q_PROPERTY :
- running - це змінна, з допомогою якої здійснюватиметься управління циклом у методі run() і буде впливати на завершення виконання корисної роботи об'єкта.
- message - це рядок, який передаватиметься для виведення в qDebug() з головного вікна програми
- 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). Не подскажите, как с этим бороться?
А что делали? Повторяете урок или как? Пытались просто скачать проект в конце статьи и запустить?
Взял Ваш пример для Qt4.8 (не спрашивайте почему). переписал связь сигнал/слот след. образом:
"thread 1" "" 1752
"thread 2" "" 1747
"thread 1" "" 1753
"thread 2" "" 1748
"thread 1" "" 1754
т.е. связь между потоками не работает.
Вижу еще волшебное слово Qt::DirectConnection, пробовал добавить - нет результата
Добавил еще в pro файл запись CONFIG += no_keywords - тоже не помогло
Что подскажите
PS: я нубер в Qt и плюсах
Не вижу всего кода, по этому куску кода невозможно сказать, в чём ошибка. На первый взгляд код написан верно.
В остальном код как и Вашем примере
Тогда мне больше нечего Вам сказать. Давно уже не работал с Qt 4.8 и приниципиально не работаю с ним сейчас.
В qt5.6 всё нормально заработало. С 4.8 - нет
Нууу... тут уже вопрос к самому Qt4.8. Если честно, идей нет, да и копаться в deprecated коде желания тоже нет.
В моем коде с двумя потоками и выводом сообщений в 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, например,
В том и дело что хотел без мютексов обойтись. т.е. суть проблемы есть старая программа, рабочая и три потока в ней A-B-(C1...Cn) .
Я глубоко в дебри многопоточности не зарывался, но если разбираться с нюансами и поднять в памяти то, что мне говорили более опытные коллеги. То можно сделать следующие выводы:
Возможно таким же способом передать туда QTcpSocket?
Да, это должно работать. Главное при передаче данных в основные потоки защищайте данные от конкурентного доступа мьютексами.
Не получается сделать connect, получаю гору ошибок. В чем может быть дело ?
connect(&thread01, &QThread::started, Server, &myserver::run);
Ввожу 1 в Write 1, ввожу 2 в Write 2, нажимаю Start и сразу Stop. Сначала вижу очень много одинаковых строк, а потом сообщение об ошибке. При этом программа не крашится, но предупреждение настораживает... Почему так? Неужели надо &QThread::terminate заменить на &QThread::quit ? Вот что у меня вышло после. Варнинг перестал сыпаться...
Ubuntu 22, Qt 6.
"1" "" 212463
"2" "1" 253535
"1" "" 212464
"1" "" 212465
"2" "1" 253536
"2" "1" 253537
Qt has caught an exception thrown from an event handler. Throwing
exceptions from an event handler is not supported in Qt.
You must not let any exception whatsoever propagate through Qt code.
Qt has caught an exception thrown from an event handler. Throwing
exceptions from an event handler is not supported in Qt.
You must not let any exception whatsoever propagate through Qt code.
Вообще, правильнее использоать quit, поскольку terminate - это более жёсткий вариант, на тот случай, если поток схватил deadlock.
Так что в итоге вы правильно всё исправили. Статья старая, на тот момент я делал больше глупых ошибок :-)
После того, как было сделано
Объект exampleObject_1 стал жить в своей ните, а не в той же, где живет MainWindow
Почему же в методе MainWindow::on_write_1_clicked()
Можно обращаться к методам exampleObject_1?
Разве могут взаимодействовать объекты из разных нитей как-то, кроме как через сигнал-слоты?
В продолжение моего предыдыщуего поста.
Похоже, нельзя обращаться к полям и методам объекта А из объекта Б, если они в разных нитях живут. Поэтому вместо
Следует писать
А вместо
Стоит писать
И на всякий случай выскажусь по поводу конструктора ExampleObject::ExampleObject(). Сейчас он пустой, но нужно проговорить. В этом конструкторе ни в коем случае нельзя выделять память из кучи (делать new), подробности тута см "NEVER allocate heap objects (using new)".
Там всё равно есть ссылочная информация на этот объект, просто... да... всё может посыпаться в более сложной логике. Пример очень старый, может быть неактуален или в чём-то неправилен в настоящее время.
"Запуск выполнения метода run будет осуществляться по сигналу запуска от соответствующего потока"
Мой душнила шепчет, что корректнее было бы написать
"Запуск выполнения метода run будет осуществляться по сигналу запуска от соответствующего объекта (thread_1 или thread_2) QThread."
Почему это важно? Может показаться, что существуют какие-то статические сигналы, которые способен emit непосредственно класс QThread. По аналогии со статическими методами...
Верно?
Ну да, так конечно можно и до столба докопаться, но с другой стороны точность не бывает лишней в некоторых вопросах :)
Вот тут можно посмотреть аналогичный современный проект (правда с одной дополнительной нитью).
Я поясню свой вопрос. Выше я писал
"Почему же в методе MainWindow::on_write_1_clicked()
Можно обращаться к методам exampleObject_1?
Разве могут взаимодействовать объекты из разных нитей как-то, кроме как через сигнал-слоты?"
Вот чего я боюс:
"Ещё совсем уж вскользь упомяну, что реализация SMP в процессорах Интел совершенно всепрощающа и гарантирует программисту, что все процессоры видят память одинаково, хотя и имеют раздельные кеши. То есть обеспечивают так называемую когерентность кешей процессоров в многопроцессорной системе.
Это не везде так. Есть процессоры, в которых надо предпринимать специальные усилия для того, чтобы данные гарантированно были синхронизированы между процессорами. Или явно выключать кеширование для страниц памяти, в которых лежат данные, доступные нескольким процессорам. Это уже тема для совершенно отдельного разговора, здесь я лишь упомянул для полноты."
Ответ Евгения "Там всё равно есть ссылочная информация на этот объект" как-то не очень успокаивает... Есть ссылка на область (какой-то) памяти, с чего бы вдруг одной и той же для разных-то нитей?..
Могут. Выполняя оператор new , Вы выделяете под объект память в куче (heap), и если вы передаёте указатель на этот объект в 10 потоков, то код во всех 10-ти потоках может работать с этим указателем и модифицировать объект. В этом случае важным является защищать процесс записи мьютексами, чтобы не возникло состояниее гонки и данные не накрылись медным тазом. А сигнал слотовое соединение - это механизм потокобезопасной передачи данных, но это абсолютно не означает, что через это соединение нельзя передать указатель на объект в другом потоке, и не означает, что не нужно записываемые данные защищать мьютексами.
А касательно той статьи на Хабре, то это системное программирование на микроконтроллерах с использованием чистого Си, судя по коду, может я и ошибаюсь, но меня берут сомнения, что там примеры на С++. Да и автор в комментариях упоминает о Atmega 128.
Поэтому и рассуждение на тему организации памяти в процессоре исходя из статьи для систем реального времени на микроконтроллерах хоть и применимо для десктоп приложений, для которых в первую очередь разрабатывался Qt, тем не менее всё же выглядит высасыванием проблемы из пальца. Хотя бы потому, что в таком случае нужно уже говорить относительно конкретной модели процессора, на котором происходит разработка. В противном случае разговор является беспредметным.