MyTetra Share
Делитесь знаниями!
О Многопоточности в 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. Он представляет собой обёртку для потоков операционной системы и предоставляет кросс-платформенный интерфейс для работы с ними.


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


Запустить код в отдельном потоке можно двумя способами:


  1. создание обёртки для нашего класса, который будет жить в отдельном потоке;
  2. переопределение метода run() в унаследованном от 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. Каковы их достоинства и недостатки?


  1. Второй способ требует меньшего количества строк кода, и на один объект меньше. Он проще в реализации, но позволяет выполнять лишь один метод.
  2. Поскольку только один метод находится в другом потоке в первом способе, то остальные методы могут быть использованы для управления тяжелым объектом из других потоков. Этот факт может выступать как достоинством, так и недостатком этого метода.
  3. Например, при использовании семафоров или мьютексов во втором способе происходит блокировка всего потока, но т. к. в этом случае в отдельном потоке живет лишь один метод, то остальные могут быть вызваны в других потоках для модификации состояния объекта.
  4. С другой стороны этот способ инкапсулирует мьютексы и другие управляющие элементы внутри одного объекта, позволяя управлять ими только через методы. В обоих случаях (3 и 4) программисту следует уделить много внимания такой структуре, т. к. в некоторых случаях можно заморозить другой поток, в котором вызывается блокирующий метод тяжелого класса.
  5. Первый способ создает, выполняет и уничтожает тяжелый объект внутри себя. Такой подход более потокобезопасен т. к. жизненный цикл объектов не зависит от внешних потоков. Также весь объект целиком замирает в ожидании при засыпании потока и управление может быть осуществлено только через внешние объекты, ссылки на которые переданы в тяжелый метод.


Исходя из выше написанного, можно заключить, что ни один из них не является панацеей и не даёт 100% гарантий, но «наломать дров» в первом способе чуть сложнее, чем во втором, если у вас большой и сложный объект. Однако, в случае когда у вас маленькая функция, можно смело использовать второй метод. Окончательное решение за выбор того или иного метода — за вами! Всего доброго!


Так же в этом разделе:
 
MyTetra Share v.0.59
Яндекс индекс цитирования