вторник, 11 марта 2014 г.

Qt основы многопоточности

Правильная реализация многопоточности Qt

В старых учебниках по Qt (того же Шлее) указывается, что для создания потока, необходимо унаследоваться от класса QThread, перегрузив виртуальный метод void run(), в духе Java. Проблема в том, что с некоторого времени класс QThread перестал быть чисто виртуальным (абстрактным), что уже не требует реализации метода run() и какого-либо наследования. Специалисты и документация утверждают, что QThread является вполне себе интерфейсом к объекту потока, и что наследование от него обязательно приведет к беде при использовании сигналов и слотов базового класса.


Решение проблемы

Предлагается использовать некий сторонний класс (его обычно зовут Worker и наследуют от QObject для использования сигналов со слотами) со слотовым методом, который стоит передать в созданный объект потока (QObject::moveToThread(QThread)) и связать тот слотовой метод с сигналом started() потока.

class Worker :
    public QObject
{
    Q_OBJECT

public:
    Worker(QObject *parent=nullptr) 
        : QObject(parent) { }

public slots:
    void process()
    {
        // Работа в потоке
        // ...
        emit finished(); // Работа завершена
    }

signals:
    void finished();
};

Создаем и запускаем поток:

void makeThread()
{
    auto *thread = new QThread;
    auto *worker = new Worker;
    worker->moveToThread(thread); // перемещаем объект в поток

    // Start
    QObject::connect(   thread, SIGNAL( started()),
                        worker, SLOT(   process()) );
    // Stop
    QObject::connect(   worker, SIGNAL( finished()),
                        thread, SLOT(   quit()) );

    // Delete
    QObject::connect(   worker, SIGNAL( finished()),
                        worker, SLOT(   deleteLater()) );
    QObject::connect(   thread, SIGNAL( finished()),
                        thread, SLOT(   deleteLater()) );

    thread->start();
}

То есть, у нас есть класс Worker в новом потоке, управляющий этим-самым потоком через сигналы. В данном случае он приказывает ему завершиться, когда сделает свою работу (высылает сигнал finished в конце метода process, который становится, в этом качестве, аналогом функции main для отдельного объекта потока QThread).
Увы, просто так это не попробуешь, потому что основной поток все норовит прекратить свое существование, попутно прибивая дочерние потоки, нохально не обращая внимание, что они там чем-то заняты. Можно устроить ему огромный тайм-аут через QThread::getCurrentThread()->sleep(время_в_секундах), примерно оценив необходимости. Можно создать объект QApplication и запустить его вечный цикл обработки сообщений. Но как потом вызвать его слот quit()? Это все не круто. Нам нужно знать, когда потоки "кончатся", а лишь потом завершать приложение.

Велосипеды

Будем пилить менеджер потоков, который будет создавать потоки, уметь их опрашивать и, в конце концов, сообщать объекту приложения QApplication, что ждать больше нечего, можно умирать.

Совместное использование данных и мьютексы

Краткое отступление по теме синхронизации доступа к данным из разных потоков. Дело в том, что потоки, в отличае от процессов, имеют доступ к одному и тому же адресному пространству (в рамках процесса, в котором они созданы), следовательно, и ко всем переменным, даже локальным. Представьте ситуацию, один поток находится в методе, в начале инициализировал переменные, затем накопил в них какой-то результят, который готовится вернуть. Тут, внезапно, в метод врывается другой поток, снова инициализирует переменные и начинает в них копить свои результаты. Первый поток из метода выносит ошибочный результат. Эта ситуация зовется "Data races". Требуется синхронизация, т.е. заставить второй поток ждать, пока первый не закончит свое дело. Здесь помогут примитивы синхронизации: критические секции, мьютексы, симофоры и другие. Сейчас о мьютексах. Это такой объект, которым может владеть только один поток. Второй поток, попытавшись захватить мьютекс, должен ждать, пока тот не освободится. В Qt мьютекс представлен классом QMutex, имеет два важных метода захвата и освобождения - lock() и unlock() соответственно. Например, заходя в критическую область, поток вызывает lock(), делает свое черное дело, вызывает unlock(). Далее будет пример работы с менеджером потоков, в котором будут хранится указатели на потоки, проверяться их состояние, также там будет общий для них мьютекс. Потоки будут бороться за доступ к выводу на консоль.

#include <QApplication>
#include <QThread>
#include <QMutex>
#include <QMutexLocker>
#include <QString>
#include <QDebug>

#include <vector>

class Worker :
    public QObject
{
    Q_OBJECT

    QMutex  *mutex_;   
    QString name_;


public:
    Worker(const QString &name, QMutex *mutex, QObject *parent=nullptr) 
        : QObject(parent), mutex_(mutex), name_(name) { }

public slots:
    void process()
    {
        for (int i=0; i<2; ++i)
        {
            mutex_->lock(); // Синхронизируем доступ к консоли

            const QThread *curThread = QThread::currentThread();
            qDebug() << name_ << " (" << i << ")";
            curThread->sleep(1);
            
            mutex_->unlock(); // Освобождаем мьютекс
        }
        emit finished();
    }

signals:
    void finished();
};



class ThreadPool : 
    public QObject
{
    Q_OBJECT

    QMutex                  consoleMutex_;
    std::vector<QThread*>   threads_;

    bool isAllFinished() 
    {
        for (auto pThread : threads_)
            if ( pThread->isFinished() == false )
            {
                qDebug() << ">>> NO";
                return false;
            }
        qDebug() << "All threads finished!";
        return true;
    }

public:
    void makeThread(const QString &name)
    {
        auto *thread = new QThread;
        auto *worker = new Worker(name, &consoleMutex_);
        worker->moveToThread(thread);
        threads_.push_back(thread);

        // Start
        QObject::connect(   thread, SIGNAL( started()),
                            worker, SLOT(   process()));
        // Stop
        QObject::connect(   worker, SIGNAL( finished()),
                            thread, SLOT(   quit()));
        QObject::connect(   worker, SIGNAL( finished()),
                            this,   SLOT(   updateStatus()));

        // Delete
        QObject::connect(   worker, SIGNAL( finished()),
                            worker, SLOT(   deleteLater()));
        QObject::connect(   thread, SIGNAL( finished()),
                            thread, SLOT(   deleteLater()));

        thread->start();
    }    
    
public slots:
    void updateStatus()
    {
        QMutexLocker locker(&consoleMutex_);
        qDebug() << ">>>>> UPDATE";
        if (isAllFinished())
        {
            qDebug() << "All threads finished!";
            QThread::currentThread()->sleep(3);
            emit allFinished();
        }
    }

signals:
    void allFinished();
};

int main(int argc, char *argv[])
{
    QApplication app(argc, argv);

    ThreadPool thPool;

    QObject::connect(   &thPool,    SIGNAL( allFinished()),
                        &app,       SLOT(   quit()) );

    thPool.makeThread("Thread #1");
    thPool.makeThread("Thread #2");
    thPool.makeThread("Thread #3");
    thPool.makeThread("Thread #4");
    thPool.makeThread("Thread #5");

    return app.exec();
}

#include "release/main.moc"

Также здесь в методе объект ThreadPool::updateStatus() создается объект QMutexLocker, который автоматически вызывает lock() при конструировании и unlock() при уничтожении (при выходе из области видимости) мьютекса, указатель на который передается в конструктор. Кроме того, ThreadPool при завершении каждого потока проверяет статус остальных, и, в том случае, если все завершились, высылает сигнал allFinished, который и позволяет завершиться обработке сообщений объекта QApplication (см. ф-ю main).

5 комментариев:

  1. Добрый день!

    Уточните пожалуйста следующий момент. НАсколько я знаю, если мы не наследуемся от класса QThread, то мы не можем использовать его protected метод sleep.
    Этот код у меня не рабочий.
    const QThread *curThread = QThread::currentThread();
    curThread->sleep(1);

    ОтветитьУдалить
  2. Анонимный24 июня 2021 г., 09:21

    VarangaOfficial - купить препарат варанга - только достоверные и проверенные факты. Воспользовавшись нашим сайтом, вы сможете узнать обстоятельную информацию касательно этого натурального лекарственного комплекса. Увидеть данные о клиническом тестировании геля, прочесть отзывы реальных пользователей и медицинского персонала. Ознакомиться с инструкцией по использованию, прочесть особенности и методы работы комплекса, понять, почему крем Варанга настолько эффективен, где можно заказать оригинальный сертифицированный препарат и, как не нарваться на подделку. Мы очень тщательно проверяем размещаемые на сайте данные. Предоставляем нашим пользователям сведения, которые были почерпнуты исключительно из авторитетных источников. Если вы обнаружили признаки грибкового поражения стоп или же долго и безрезультатно стараетесь избавиться от этого коварного недуга, на нашем сайте вы отыщете быстрый и легкий способ устранения проблемы. Приобщайтесь и живите здоровой полноценной жизнью. Все, что вы хотели знать, теперь можно найти на одном ресурсе.

    ОтветитьУдалить
  3. Посетив нашу поисковую систему по сексу на веб-ресурсе, вы получите исключительно отличные и отличные порно видео в этом разделе, а также программы для анальных пар bonga cams по запросу на секс чат со зрелыми женщинами. На нашем ресурсе в каталоге собраны сливки секс-контента. Введя порно-поисковый запрос bonga cams anal couple, вы быстро насладитесь выбранным видео без регресса и без лишних жестов. На портале очень мало рекламаций, поэтому ничто другое не может помешать нам, чтобы насладиться качественным секс-видео по поисковому запросу bonga cams anal couple. Когда, согласно вашему порно запросу, у анальных пар bonga cams нет ни одного порно видео, не стесняйтесь писать нам в режиме мгновенного общения, и наши сотрудники немедленно исправят эту досадную ситуацию.

    ОтветитьУдалить
  4. Вам срочно нужны финансы для схемы с мгновенным решением, но вы не хотите давать высокие процентные ставки? Не смотри дальше! " Льготный кредит." - Наши специалисты имеют большой опыт и точно знают, где и где можно мгновенно использовать виртуальный кредит на дебетовую карту во всех регионах российской федерации, а также вам не придется съезжать с этого момента, наши сотрудники сделают все самостоятельно бесплатно. Минимальное начисление процентов, за которое вы сможете получить материальные средства с помощью наших советов, составляет 0%. " Льготный кредит.Com" является агрегатором лучших мфо (мкк и мфк) на внутреннем рынке, которые заслужили безупречную репутацию среди заемщиков. Мы - команда мастеров с опытом работы ("com-кредит"), у нас есть налаженный безопасный канал связи с любым таким центром, куда мы отправляем заказы на предоставление микрокредитов онлайн. Предварительно ваша информация анализируется специальным алгоритмом (на машине и только после этого отправляется в соответствующие mcc и ifc, всего в стране насчитывается около 20 таких партнеров. В то же время время, когда будет выдано одобрение, потребует около четверти часа (часто быстрее). После отправки запроса вам нужно только найти ответ на сигнал администратора (несколько таких звонков могут сработать, если в разных компаниях одобрено сразу 10 микрокредитов), чтобы уточнить прихоти и авторизацию, и мгновенно получить свои средства удобным для вас способом. Если в течение часа звонка, а также смс от кредитора так и не поступило, вполне возможно, что заявка не была одобрена. В таких обстоятельствах мы рекомендуем прибегнуть к его отправке независимо от исходных сайтов компаний (их коллекция специально подобрана для посетителей и предоставляется в процессе рассылки заявки). Если пользователи уже закрыли страницу, вы можете выбрать мфо из списка здесь. Мы настоятельно рекомендуем вам не использовать ложную информацию в форме, потому что вся предупреждаемая информация будет проверена на стороне парней, и если будут обнаружены ложные данные, ваш заказ будет отклонен ими, и все ваши дальнейшие заказы смогут автоматически отклоняться, и не один отправленный от нас, а в целом. Это информация о заработке, которая попала не в те руки (разумно отметить небольшую, но реальную зарплату, а не большую ложную). Услуга льготного кредитования.Com" работает в режиме 7-24 на территории российской федерации. Не слишком уверенная кредитная биография - избыточный вес - это не приговор! Но тогда ставка будет довольно высокой, и не более 1% за 24 часа, чтобы быть меньше, займ без паспорта Случается очень редко). Мы делаем все возможное, чтобы потребители имели возможность использовать сумму с плохой кредитной историей на максимально выгодных условиях.

    ОтветитьУдалить