Отправляет email-рассылки с помощью сервиса Sendsay

Статьи по Visual C++

  Все выпуски  

Особенности работы с потоками в QT


Домашняя страница www.devdoc.ru

DevDoc - это новые статьи по программированию каждую неделю.

Заходи и читай!

Домашняя страница Письмо автору Архив рассылки Публикация статьи

Выпуск №64

Здравствуйте уважаемые подписчики, сегодня в номере:

  • Результаты опроса
  • Статья "Особенности работы с потоками в QT"

Результаты опроса

Опросы постоянно проводятся на сайте www.devdoc.ru.

Результаты опроса
Для какой платформы вы пишите программы

1Windows
48% ( 485 )
 
2Linux
24% ( 241 )
 
3Mac
3% ( 27 )
 
4Мобильные устройства
25% ( 249 )
 

Всего голосов: 1002
Последний голос отдан: Среда - 4 Мая 2011 - 12:28:43


Постоянная ссылка на статью (с картинками): http://www.devdoc.ru/index.php/content/view/qt_threads.htm

Автор: Кудинов Александр
Последняя модификация: 2011-05-04 15:41:58

Особенности работы с потоками в QT

Введение

Меня побудило написать эту статью серия тестовых заданий, которые я проверял. Я был крайне удивлен, что почти все они содержали однотипные ошибки. Многопоточное программирование по праву считается одной из сложных вещей для понимания. Но помимо концепции надо очень хорошо понимать особенности инструмента, который используется. В QT взаимодействие потоков организовано не совсем очевидным на первый взгляд образом. Оно несколько отличается от той модели потоков, которая представлена в native API операционных систем. Но если разобраться, то в QT все логично.

Все примеры в этой статье содержат только минимум кода для демонстрации концепции. Для компиляции их надо доработать.

QThread

Потоки в QT представлены классом QThread. Есть несколько вариантов выполнить код в потоке. Простой способ, это перегрузить метод QThread::run(), который вызывается в контексте нового потока. Мы будем рассматривать именно этот случай в наших примерах.

Есть еще классы QThreadPool и QRunnable, которые позволяют организовать пул потоков и снизить издержки на создание новых потоков. На этих классах я не буду подробно останавливаться. Если вы разберетесь в общей концепции работы потоков - то их использование будет тривиальным.

Пример, того как выполнить простейший код в другом потоке:

class MyThread : public QThread
{
 Q_OBJECT
public:
 
 virtual void run()
 {
   //делаем что то в контексте нового потока
 
  exec();   //запускаем обработку очереди сообщений потока
 }
};
 
void createThread()
{
 MyThread *thread = new MyThread();
 thread->start();   //запускаем поток. После запуска, выполнится метод MyThread::run()
}

Архитектура QThread похожа на классы из других библиотек, но это совпадение только внешнее. Дальше начинаются тонкости. В первую очередь это касается очереди сообщений. Поток может и не использовать очередь сообщений. В этом случае у потомка QThread невозможно будет использовать механизм signal/slot в полной мере. Поток будет способен выбрасывать сигналы, а вот слоты работать не будут.

Если требуется работа со слотами или использовать некоторые объекты внутри потока (например, QNetworkAccessManager), то поток обязан иметь очередь сообщений.

Итак, самое простое QT приложение имеет главный поток и очередь сообщений, которая работает в контексте этого потока:

int main(int argc, char *argv[])
{
     //функция выполняется в контексте главного потока
    QApplication a(argc, argv);
    QDialog myDialog;
    myDialog.show();
 
    return a.exec();  //запускаем очередь сообщений, которая работает в главном потоке.
}

Идем дальше. Все потомки QObject "принадлежат" какому-то потоку. В примере выше, мы создали объект myDialog в контексте главного потока. По умолчанию, QT считает, что объект принадлежит тому потоку, в котором он был создан. Если при создании указать объекту родителя, который принадлежит другому потоку, то и новый объект будет принадлежать другому потоку.

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

Прежде чем мы продолжим, хочу сказать несколько слов о слотах. Посмотрим в документацию:

bool QObject::connect ( const QObject * sender, const char * signal, 
   const QObject * receiver, const char * method,
   Qt::ConnectionType type = Qt::AutoConnection )

Нас интересует последний параметр. Если мы оставим его по умолчанию, то это значит следующее:

  • если мы соединяем объекты, которые принадлежат одному потоку, то тип подключения аналогичен Qt::DirectConnection
  • если мы соединяем объекты, которые принадлежат разным потокам, то тип подключения аналогичен Qt::QueuedConnection

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

Можно указывать тип подключения напрямую. Мы можем использовать Qt::QueuedConnection даже для объектов в одном потоке, если по какой то причине надо вызвать слот через очередь сообщений.

Теперь я покажу все это на примерах, чтобы было понятней.

class MyThread : public QThread
{
 Q_OBJECT
public:
 
 virtual void run()
 {
   //делаем что то в контексте нового потока
 
  exec();   //запускаем обработку очереди сообщений потока
 }
 
protected slots:
 void  receiveSignal();
};
 
class MyDialog : public QDialog
{
 Q_OBJECT
 
signals:
 void clickButton();   //Диалог выкинет этот сигнал, когда пользователь нажмет на кнопку в окне.
}
 
 
int main(int argc, char *argv[])
{
     //функция выполняется в контексте главного потока
    QApplication a(argc, argv);
    MyDialog myDialog;
    myDialog.show();
 
    MyThread thread;
    
    connect(&myDialog, SIGNAL(clickButton()), &thread, SLOT(receiveSignal()));
 
    return a.exec();  //запускаем очередь сообщений, которая работает в главном потоке.
}

Вот мы дошли до первого сюрприза QT. Мы создаем объект thread в контексте главного потока, поэтому он и будет его владельцем. Это выглядит нелогично для объекта, который сам представляет поток, но об этом надо постоянно помнить. Для нас это означает, что когда пользователь нажмет кнопку на диалоге, то у потока вызовется слот receiveSignal() в контексте главного потока, а не в контексте рабочего потока thread!

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

Перепишим:

class MyThread : public QThread
{
 Q_OBJECT
public:
 
 MyThread(QObject * parent = 0 ) : QThread(parent)
 {
  moveToThread(this);
 }
 
 virtual void run()
 {
   //делаем что то в контексте нового потока
 
  exec();   //запускаем обработку очереди сообщений потока
 }
 
protected slots:
 void  receiveSignal();
};

Мы добавили в конструктор вызов moveToThread(this). Выглядит странно, не так ли? На самом деле мы меняем поток, который владеет объектом. Т.е. теперь объект thread начнет использовать свою очередь сообщений.

Теперь если пользователь нажмет на кнопку на форме - информация о сигнале попадет в очередь сообщений рабочего потока MyThread, а затем будет вызван слот receiveSignal() в его же контексте. Бинго! Это то, что нам и требовалось.

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

class MyThread : public QThread
{
 Q_OBJECT
public:
 
 MyThread(QObject * parent = 0 ) : QThread(parent)
 {
  m_pNAM = new QNetworkAccessManager();
  connect(m_pNAM, SIGNAL(finished ( QNetworkReply *)), this, SLOT(finishRequest(QNetworkReply *)));
 }
 
 virtual void run()
 {
   //инициируем загрузку страницы
  m_pNAM->get(QUrl("http://www.google.com"));
 
  exec();   //запускаем обработку очереди сообщений потока
 }
 
protected slots:
 void  finishRequest(QNetworkReply *pReply)
 {
  //Получаем содержимое страницы и удаляем запрос
  //Тут еще хорошо было бы добавить обработку ошибок.
  QByteArray page = pReply->readAll();
  pReply->deleteLater();
 }
private:
 
 QNetworkAccessManager *m_pNAM;
};

Давайте посмотрим, то происходит. Мы создаем объект типа QNetworkAccessManager (далее NAM) в конструкторе. Т.к. MyThread изначально принадлежит главному потоку приложения, то и новый объект наследует это свойство.

Дальше мы инициируем загрузку страницы в методе run(). По завершению операции NAM выбросит сигнал finished и будет вызван слот finishRequest. QNetworkAccessManager использует очередь сообщений для выполнения сетевых операций. А так как он принадлежит главному потоку, то он будет использовать очередь сообщений главного потока. Иными словами все сетевые операции будут выполняться в главном потоке, а не в рабочем как можно было ожидать.

Более того, слот finishRequest тоже вызовется в главном потоке. Т.е. если мы будем тут выполнять "тяжелую" операцию, мы затормозим пользовательский интефейс. Вызов в конструкторе moveToThread(this) - не решит проблему, потому что m_pNAM по прежнему создается в контексте главного потока. Надо еще добавить m_pNAM->moveToThread(this). Это заставит NAM использовать очередь сообщений нашего потока.

Т.е. правильный вариант будет:

MyThread ::MyThread(QObject * parent = 0 ) : QThread(parent)
{
 moveToThread(this);
 m_pNAM = new QNetworkAccessManager();
 m_pNAM-> moveToThread(this);
 
 connect(m_pNAM, SIGNAL(finished ( QNetworkReply *)), this, SLOT(finishRequest(QNetworkReply *)));
}

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

А теперь вариация на ту же тему, но пример более интересный:

class MyThread : public QThread
{
 Q_OBJECT
public:
 
 MyThread(QObject * parent = 0 ) : QThread(parent)
 {
 }
 
 virtual void run()
 {
  m_pNAM = new QNetworkAccessManager();
  
  connect(m_pNAM, SIGNAL(finished ( QNetworkReply *)), this, SLOT(finishRequest(QNetworkReply *)));
 
   //инициируем загрузку страницы
  m_pNAM->get(QUrl("http://www.google.com"));
 
  exec();   //запускаем обработку очереди сообщений потока
 }
 
protected slots:
 void  finishRequest(QNetworkReply *pReply)
 {
  //Получаем содержимое страницы и удаляем запрос
  //Тут еще хорошо было бы добавить обработку ошибок.
  QByteArray page = pReply->readAll();
  pReply->deleteLater();
 }
private:
 
 QNetworkAccessManager *m_pNAM;
};

Теперь мы создаем m_pNAM в методе run(). Т.е. объект у нас создается в контексте нашего потока, а это значит, что он будет использовать очередь сообщений потока MyThread. Но обратите внимание, что сам класс MyThread по прежнему принадлежит другому потоку.

Поэтому NAM будет выполнять все свои внутренние операции в контексте рабочего потока, но при этом сигнал QNetworkAccessManager::finished(QNetworkReply*) вызовет слот finishRequest(QNetworkReply*) в контексте главного потока. Иногда это может быть полезно, но чаще нет.

Если у вас остались вопросы - спрашивайте.

Copyright (C) Kudinov Alexander, 2006-2010

Перепечатка и использование материалов запрещена без писменного разрешения автора.


В избранное