|
|||||||
О Многопоточности в Qt и как создать поток
Время создания: 30.03.2023 11:02
Автор: Илья Житенёв
Текстовые метки: qt, c++, поток, многопоточность, многопоток, QThread
Раздел: Компьютер - Программирование - Язык C++ (Си++) - Библиотека Qt - Многопоточность
Запись: xintrea/mytetra_syncro/master/base/1680163371cmbv316am4/text.html на raw.github.com
|
|||||||
|
|||||||
Многопоточность — свойство платформы выполнять код внутри одного процесса может выполнятся “параллельно” без предписанного порядка во времени. Такой подход полезен когда отделить ресурсоёмкую задачу от остального кода. Например, читать с диска в память большой файл и не «заморозить» графический интерфейс программы. В этой статье поговорим о многопоточности в C++ и о том, как это работает во фреймворке Qt. На примере покажу как запустить в отдельном потоке только один метод объекта, оставив остальные методы «снаружи», обсудим плюсы и минусы такого подхода и как поступить в такой ситуации! Содержание статьи 1. Как устроена многопоточность в Qt? 2. Создание потока через обертку 3. Переопределение метода run() класса QThread 4. Достоинства и недостатки 1. Как устроена многопоточность в Qt?В Qt потоками управляет класс QThread. Он представляет собой обёртку для потоков операционной системы и предоставляет кросс-платформенный интерфейс для работы с ними. Помните, что один объект отвечает за один поток, поэтому не стоит бездумно плодить потоки. Создание потока сопряжено с существенными накладными расходами в программе. Запустить код в отдельном потоке можно двумя способами :
Важно! Выделение памяти оператором new для экземпляров класса необходимо выполнять в том потоке, в котором они будут исполнятся. Это правило делает собственником объектов тот поток, который их создал. Так легче контролировать жизненный цикл объектов и позволит избежать ошибок в работе, когда используемый объект внезапно был удален в другом потоке. 2. Создание потока через обёрткуДопустим, что вся тяжелая работа выполняется в методе doWork() класса HeavyWork, поэтому необходимо перенести его в отдельный поток. Код класса представлен ниже: #include <iostream> class HeavyWork : public QObject { Q_OBJECT public: void doWork() { std::cout << "Начинаем сложную работу"; // Для эмуляции долгого выполнения сложной операции // отправим поток в сон на 1 секунду QThread::sleep(1); std::cout << "Всё готово!"; } }; Экземпляр такого класса необходимо создать внутри потока. Напрямую сделать этого не возможно, поэтому создадим обработчик, который и будет создавать, взаимодействовать и уничтожать такой объект. Создаем класс Worker с одним методом и сигналом, излучаемым с флагом типа bool, оповещающем об (не) успешности выполненной работы. Все это происходит внутри метода process(): #include <QObject> class Worker : public QObject { Q_OBJECT HeavyWork* work; public slots: void process(); signals: void finished(bool); };
// Этот метод будет запущен при старте потока void Worker::process() {
// Аллоцируем наш объект. Теперь это происходит в отдельном потоке work = new HeavyWork(); if(work == nullptr) { // Если произошла ошибка, то сигнализируем что поток завершен // с отрицательным результатом и покидаем функцию // (а с ней и завершается поток) emit finished(false); return; } // Делаем сложную работу work->doWork(); // Сигнализируем об успешном выполнении emit finished(true); } На этом подготовительные операции выполнены и переходим к перемещению обработчика Worker в отдельный поток. Для этого создадим ещё один объект Controller, внутри которого вся магия и произойдет: экземпляр обработчика Worker и поток начнут взаимодействовать. class Controller : public QObject { Q_OBJECT public: void makeThread(); }; Единственный метод класса – makeThread(), вот он то нам и нужен, давайте его пошагово реализуем! 1. Создаем экземпляры обработчика Worker и экземпляр потока QThread Worker* worker = new Worker(); QThread* thread = new QThread(); 2. Настроим и передадим данные, если нужно, экземпляру Worker. В этой точке у нас есть последний шанс сделать это привычным путем 3. Перемещаем worker в новорожденный поток worker->moveToThread(thread); Налаживаем связь между потоком и обработчиком. Ключ на старт! // При запуске потока запускаем выполнение метода Worker::process() connect(thread, &QThread::started, worker, &Worker::process); // При излучении сигнала finished получаем флаг успешности // и выводим в консоль соответствующее сообщение connect(worker, &Worker::finished, this, [](bool state){ if(state) std::cout << "Успех" << std::endl; else std::cout << "Хьюстон, у нас проблемы!" << std::endl; }); // Также, по сигналу finished отправляем команду на завершение потока connect(worker, &Worker::finished, thread, &QThread::quit); // А потом удаляем экземпляр обработчика connect(worker, &Worker::finished, worker, &QObject::deleteLater); // И наконец, когда закончит работу поток, удаляем и его connect(thread, &QThread::finished, thread, &QObject::deleteLater); 3..2..1.. Поехали! Запускаем выполнение потока thread->start(); Реализация метода makeThread() завершена. Для проверки выполните следующий код и наблюдайте за стандартным выводом приложения: Controller ctrl; ctrl.makeThread(); 3. Переопределение метода run() класса QThreadВторой способ заключается в наследовании класса QThread и переопределении метода run(). Однако, этот метод нарушает принципы SOLID в подавляющем большинстве случаев, поэтому пойдем окольными путями Обновлено 23.02.22 Информация о нарушении принципов SOLID не совсем корректна. Подробнее об этом способе создания потоков я рассказал в статье Многопоточность в Qt через наследование QThread Вместо наследования создадим свой класс и передадим один из его методов на выполнение в отдельный поток. В качестве нагрузки используем уже знакомый класс HeavyWork, а выполнять работу будем в классе AnotherController class AnotherController : public QObject { Q_OBJECT public: void makeThread(); }; , где метод makeThread() реализуем следующим образом: 1. Создаем экземпляр класса HeavyWork HeavyWork* work = new HeavyWork(); 2. Создаем поток через статический метод QThread::create() принимающий Function в качестве параметра. Function — это адаптер функциональных объектов. Создать адаптер можно с помощью функции std::bind из стандартной библиотеки шаблонов (STL). Этот адаптер будет запущен при запуске потока. Синтаксис следующий: QThread* thread = QThread::create(std::bind(&HeavyWork::doWork, work)); Первым аргументом будет указатель на метод doWork класса HeavyWork, а далее передаем контекст — указатель на конкретный класс, метод которого будет запущен. Нечто похожее проделывали в методе connnect для сигналов и слотов. Далее через запятую указываются аргументы, если они нужны, которые будут переданы в метод doWork, но в нашем случае у метода doWork аргументы отсутствуют. 3. Налаживаем связь connect(thread, &QThread::finished, work, &QObject::deleteLater); connect(thread, &QThread::finished, thread, &Qobject::deleteLater); В этом случае сигналов стало меньше, в виду того, что некоторые из них принадлежали объекту Worker, который теперь не используется, однако, ничего не мешает добавить сигналы в класс HeavyWork и связаться с ними. 4. И наконец запускаем выполнение потока. thread->start(); Метод makeThread() реализован. Запуск программы аналогичен предыдущему случаю AnotherController anCtrl; anCtrl.makeThread(); Результат выполнения также аналогичен первому способу. В вашем распоряжении теперь два способа для запуска потока и мы можем обсудить их достоинства и недостатки. 4. Достоинства и недостаткиВыше было показано два метода создания потока - через обертку и переопределением метода run() класса QThread. Каковы их достоинства и недостатки?
Исходя из выше написанного, можно заключить, что ни один из них не является панацеей и не даёт 100% гарантий, но «наломать дров» в первом способе чуть сложнее, чем во втором, если у вас большой и сложный объект. Однако, в случае когда у вас маленькая функция, можно смело использовать второй метод. Окончательное решение за выбор того или иного метода — за вами! Всего доброго! |
|||||||
Так же в этом разделе:
|
|||||||
|
|||||||
|