Qt wiki will be updated on October 12th 2023 starting at 11:30 AM (EEST) and the maintenance will last around 2-3 hours. During the maintenance the site will be unavailable.
Threads Events QObjects/ru: Difference between revisions
No edit summary |
(Дополнен перевод, актуализирована часть ссылок (на Qt 4.8)) |
||
(10 intermediate revisions by 3 users not shown) | |||
Line 1: | Line 1: | ||
{{Cleanup | reason=Auto-imported from ExpressionEngine.}} | |||
'''Русский''' [[Threads_Events_QObjects|English]] [[Threads_Events_QObjects_Chinese|中文]] [[Threads_Events_QObjects_Korean|한국어]] [[Threads_Events_QObjects_Bulgarian|Български]] | '''Русский''' [[Threads_Events_QObjects|English]] [[Threads_Events_QObjects_Chinese|中文]] [[Threads_Events_QObjects_Korean|한국어]] [[Threads_Events_QObjects_Bulgarian|Български]] | ||
Line 11: | Line 12: | ||
== Внимание: Beta версия == | == Внимание: Beta версия == | ||
Статья | Статья находится на стадии завершения, однако требует некоторой полировки и добавления хороших примеров. Обзор и сотрудничество приветствуются. Дискуссия по этой статье находится [http://developer.qt.nokia.com/forums/viewthread/2423/ здесь]. | ||
Дискуссия по русскому переводу находится | Дискуссия по русскому переводу находится [http://developer.qt.nokia.com/forums/viewthread/2601/ здесь]. | ||
= Вступление = | = Вступление = | ||
Line 20: | Line 21: | ||
В девяти из десяти случаев, беглый осмотр их кода показывает, что наибольшая проблема состоит в том факте, что они используют потоки в первый раз и попадают в бесконечные ловушки параллельного программирования. | В девяти из десяти случаев, беглый осмотр их кода показывает, что наибольшая проблема состоит в том факте, что они используют потоки в первый раз и попадают в бесконечные ловушки параллельного программирования. | ||
Легкость создания и запуска потоков в Qt, в сочетании с некоторым незнанием стилей программирования (особенно асинхронного сетевого программирования, в сочетании с | Легкость создания и запуска потоков в Qt, в сочетании с некоторым незнанием стилей программирования (особенно асинхронного сетевого программирования, в сочетании с Qt–архитектурой сигналов и слотов) и/или привычки, приобретенные при использовании других инструментариев или языков, обычно приводят к тому, что люди выстреливают себе в ногу. Кроме того, поддержка потоков в Qt - это палка о двух концах: в то время как это делает создание многопоточных приложений очень простым для вас, это добавляет определенное количество особенностей (особенно когда дело доходит до взаимодействия с QObject), о которых вы должны знать. | ||
Целью данного документа '''не''' является научить вас использовать потоки, делать правильное блокирование, использовать параллельность и писать масштабируемые программы; есть много хороших книг на эти темы; например, взгляните на список рекомендованного чтения | Целью данного документа '''не''' является научить вас использовать потоки, делать правильное блокирование, использовать параллельность и писать масштабируемые программы; есть много хороших книг на эти темы; например, взгляните на список рекомендованного чтения [https://doc.qt.io/archives/qt-4.8/threads.html на этой странице]. Вместо этого, эта небольшая заметка предназначена для введения пользователей в потоки Qt 4, для того чтобы избежать наиболее распространенных ошибок и помочь им разрабатывать код, одновременно и более надежный, и имеющий лучшую структуру. | ||
== Предпосылки == | == Предпосылки == | ||
Введение общего назначения в программирование (потоков) отсутствует, мы считаем, что вы уже обладаете | Введение общего назначения в программирование (потоков) отсутствует, мы считаем, что вы уже обладаете некоторыми знаниями об: | ||
* Основы C++ (хотя большая часть рекомендаций также подойдет и для других языков); | * Основы C++ (хотя большая часть рекомендаций также подойдет и для других языков); | ||
* Основы Qt: QObjects, сигналы и слоты, обработка событий; | * Основы Qt: QObjects, сигналы и слоты, обработка событий; | ||
* Что такое поток, и какие существуют отношения между потоками, процессами и операционной системой; | * Что такое поток, и какие существуют отношения между потоками, процессами и операционной системой; | ||
* Как запустить и остановить поток, или подождать пока он завершиться, (по крайней мере) под одной известной ОС.; | * Как запустить и остановить поток, или подождать пока он завершиться, (по крайней мере) под одной известной ОС.; | ||
* Как использовать мьютексы, семафоры и ожидать условия для создания | * Как использовать мьютексы, семафоры и ожидать условия для создания потоко–безопасных/reentrant функций, структур, классов. | ||
В этом документе мы будем следовать | В этом документе мы будем следовать [https://doc.qt.io/archives/qt-4.8/threads-reentrancy.html терминологии] Qt, В которой сказано: | ||
* '''Реентерабельный''' Класс является реентерабельным, если безопасно использовать его экземпляры более чем из одного потока, при условии, что не более одного потока имеют доступ к экземпляру одновременно. Функция является реентерабельной, если безопасно вызывать ее из более чем одного потока одновременно, при условии, что каждый вызов ссылается на уникальные данные. Другими словами, это означает, что пользователи этого класса/функции должны ''сериализовать'' все попытки доступа к экземплярам/общим данным средствами некоторого ''внешнего механизма блокировки''. | * '''Реентерабельный''' Класс является реентерабельным, если безопасно использовать его экземпляры более чем из одного потока, при условии, что не более одного потока имеют доступ к экземпляру одновременно. Функция является реентерабельной, если безопасно вызывать ее из более чем одного потока одновременно, при условии, что каждый вызов ссылается на уникальные данные. Другими словами, это означает, что пользователи этого класса/функции должны ''сериализовать'' все попытки доступа к экземплярам/общим данным средствами некоторого ''внешнего механизма блокировки''. | ||
Line 40: | Line 41: | ||
= События и цикл обработки событий = | = События и цикл обработки событий = | ||
События и доставка событий играют центральную роль в архитектуре Qt. В этой статье мы не рассматриваем эту тему досконально; вместо этого внимание уделяется некоторым связанным с потоками ключевым концепциям ( | События и доставка событий играют центральную роль в архитектуре Qt. В этой статье мы не рассматриваем эту тему досконально; вместо этого внимание уделяется некоторым связанным с потоками ключевым концепциям ([http://doc.qt.nokia.com/latest/eventsandfilters.html здесь] и [http://doc.qt.nokia.com/qq/qq11-events.html здесь] можно найти больше информации по событиям в Qt). | ||
'''Событием''' в Qt называется объект, представляющий | '''Событием''' в Qt называется объект, представляющий что–то интересное из произошедшего; главным отличием событий от сигналов является то, что события ''предназначены'' для конкретного объекта в нашем приложении (который решает, что с этим событием делать), а сигналы "гуляют сами по себе". С точки зрения кода все события являются объектами какого–либо подкласса [http://doc.qt.nokia.com/latest/qevent.html QEvent], и все производные от Object классы могут переопределять виртуальный метод QObject::event() для работы с событиями, предназначенными для данного объекта. | ||
События могут быть сгенерированы как внутри, так и снаружи приложения, например: | События могут быть сгенерированы как внутри, так и снаружи приложения, например: | ||
Line 69: | Line 70: | ||
* События посланные из других потоков (смотри дальше). | * События посланные из других потоков (смотри дальше). | ||
В UNIX-подобных системах, менеджер окон (то есть X11) уведомляет приложения через сокеты (Unix Domain или TCP/IP), так как клиенты используют их для общения с X сервером. Если мы решим осуществить межпоточные вызовы событий с внутренними socketpair(2), все что осталось чтобы пробудить цикл | В UNIX-подобных системах, менеджер окон (то есть X11) уведомляет приложения через сокеты (Unix Domain или TCP/IP), так как клиенты используют их для общения с X сервером. Если мы решим осуществить межпоточные вызовы событий с внутренними socketpair(2), все что осталось чтобы пробудить цикл обработки событий: | ||
* Сокеты; | * Сокеты; | ||
* Таймеры; | * Таймеры; | ||
Line 81: | Line 82: | ||
* '''Отрисовка виджетов и взаимодействие''': QWidget::paintEvent() будет вызвано при доставке объектов QPaintEvent, которые генерируются и вызовом QWidget::update() (т.е. внутренне), и оконным менеджером (например потому, что скрытое окно было показано). То же самое справедливо для всех видов взаимодействия (клавиатура, мышь и т.д.): необходимо, чтобы соответствующие события были добавлены в цикл обработки событий. | * '''Отрисовка виджетов и взаимодействие''': QWidget::paintEvent() будет вызвано при доставке объектов QPaintEvent, которые генерируются и вызовом QWidget::update() (т.е. внутренне), и оконным менеджером (например потому, что скрытое окно было показано). То же самое справедливо для всех видов взаимодействия (клавиатура, мышь и т.д.): необходимо, чтобы соответствующие события были добавлены в цикл обработки событий. | ||
* '''Таймеры''': Короче говоря, они активизируются, когда срабатывает select(2) или подобный вызов, таким образом, вы должны позволить Qt сделать эти вызовы для вас, путем возвращения в цикл обработки событий. | * '''Таймеры''': Короче говоря, они активизируются, когда срабатывает select(2) или подобный вызов, таким образом, вы должны позволить Qt сделать эти вызовы для вас, путем возвращения в цикл обработки событий. | ||
* '''Сеть''': все низкоуровневые классы Qt для работы с сетью (QTcpSocket, QUdpSocket, QTcpServer и т.д.) спроектированы асинхронными. Когда вы вызываете read(), они возвращают только уже доступные данные; когда вы вызываете write(), они добавляют задачу в список для записи позже. Фактическое чтение/запись происходят только когда вы возвращаетесь в цикл обработки событий. Обратите внимание, что они действительно предлагают синхронные методы (семейство методов waitFor*), но их | * '''Сеть''': все низкоуровневые классы Qt для работы с сетью (QTcpSocket, QUdpSocket, QTcpServer и т.д.) спроектированы асинхронными. Когда вы вызываете read(), они возвращают только уже доступные данные; когда вы вызываете write(), они добавляют задачу в список для записи позже. Фактическое чтение/запись происходят только когда вы возвращаетесь в цикл обработки событий. Обратите внимание, что они действительно предлагают синхронные методы (семейство методов waitFor*), но их использовать не рекомендуется, так как они блокируют цикл событий во время ожидания. Классы высокого уровня, типа QNetworkAccessManager, просто не предоставляют синхронного API и требуют наличия цикла событий. | ||
== Блокирование цикла обработки событий == | == Блокирование цикла обработки событий == | ||
Перед обсуждением почему '''вы никогда не должны блокировать | Перед обсуждением почему '''вы никогда не должны блокировать цикл обработки событий''', давайте попытаемся выяснить, а что такое "блокировка". Предположим что у вас есть кнопка, которая испускает сигнал соединенный со слотов нашего рабочего объекта, который делает очень много работы. После нажатия на кнопку стек вызовов будет выглядеть следующим образом (стек растет вниз): | ||
# main(int, char | # main(int, char *) | ||
# QApplication::exec() | # QApplication::exec() | ||
# […] | # […] | ||
# QWidget::event(QEvent | # QWidget::event(QEvent *) | ||
# Button::mousePressEvent(QMouseEvent | # Button::mousePressEvent(QMouseEvent*) | ||
# Button::clicked() | # Button::clicked() | ||
# […] | # […] | ||
# Worker::doWork() | # Worker::doWork() | ||
В main() мы запускаем цикл обработки событий, как обычно, вызывая QApplication::exec() (строка 2). Оконный менеджер прислал нам событие нажатия на кнопку мыши, которое бло захвачено ядром Qt, преобразовано в QMouseEvent и послано методу нашего виджета event() (строка 4) от QApplication::notify() (не показан). Так как event() не переопределен у Button, была вызвана реализация базового класса (QWidget). QWidget::event() определил событие как нажатие мыши и вызвал специализированный обработчик Button::mousePressEvent() (строка 5). Мы | В main() мы запускаем цикл обработки событий, как обычно, вызывая QApplication::exec() (строка 2). Оконный менеджер прислал нам событие нажатия на кнопку мыши, которое бло захвачено ядром Qt, преобразовано в QMouseEvent и послано методу нашего виджета event() (строка 4) от QApplication::notify() (не показан). Так как event() не переопределен у Button, была вызвана реализация базового класса (QWidget). QWidget::event() определил событие как нажатие мыши и вызвал специализированный обработчик Button::mousePressEvent() (строка 5). Мы переопределили этот метод для испускания сигнала Button::clicked() (строка 6), который вызывает слот Worker::doWork в нашем рабочем объекте (строка 7). | ||
Пока рабочий объект занят работой, что делает цикл обработки событий? Вы должны были догадаться: ничего! Он отправил нажатие кнопки мыши и ждет в блокировке пока обработчик события вернет управление. Нам удалось '''блокировать цикл обработки событий''', что означает что никакие события больше не могут посылаться, пока мы не вернемся из слота doWork(), вверх по стеку в цикл обработки событий и не позволим ему обработать ожидающие события. | Пока рабочий объект занят работой, что делает цикл обработки событий? Вы должны были догадаться: ничего! Он отправил нажатие кнопки мыши и ждет в блокировке пока обработчик события вернет управление. Нам удалось '''блокировать цикл обработки событий''', что означает что никакие события больше не могут посылаться, пока мы не вернемся из слота doWork(), вверх по стеку в цикл обработки событий и не позволим ему обработать ожидающие события. | ||
Line 102: | Line 103: | ||
При остановленной доставке событий, '''виджеты не будут обновлять себя''' (объекты QPaintEvent будут сидеть в очереди), '''невозможно дальнейшее взаимодествие с виджетами''' (по той же причине), '''таймеры не будут срабатывать''' и '''сетевые коммуникации будут замедлены и остановлены'''. Кроме того, многие оконные менеджеры обнаружат, что ваше приложение более не обрабатывает события и '''сообщат пользователю, что ваше приложение не отвечает'''. Вот почему так важно быстро реагировать на события и возвращаться к циклу обработки как можно скорее! | При остановленной доставке событий, '''виджеты не будут обновлять себя''' (объекты QPaintEvent будут сидеть в очереди), '''невозможно дальнейшее взаимодествие с виджетами''' (по той же причине), '''таймеры не будут срабатывать''' и '''сетевые коммуникации будут замедлены и остановлены'''. Кроме того, многие оконные менеджеры обнаружат, что ваше приложение более не обрабатывает события и '''сообщат пользователю, что ваше приложение не отвечает'''. Вот почему так важно быстро реагировать на события и возвращаться к циклу обработки как можно скорее! | ||
== Принудительная обработка событий. == | |||
Так что же делать, если нам необходимо выполнить длительную операцию при этом избежать блокировки очереди (цикла) сообщений? Во-первых, мы можем переместить задачу в другой поток, ниже мы рассмотрим этот вариант подробней. Во-вторых, мы имеем возможность вручную запускать процесс обработки событий периодически, вызывая QCoreApplication::processEvents() внутри блокируещй задачи. | Так что же делать, если нам необходимо выполнить длительную операцию при этом избежать блокировки очереди (цикла) сообщений? Во-первых, мы можем переместить задачу в другой поток, ниже мы рассмотрим этот вариант подробней. Во-вторых, мы имеем возможность вручную запускать процесс обработки событий периодически, вызывая QCoreApplication::processEvents() внутри блокируещй задачи. | ||
Функция QCoreApplication::processEvents() будет обрабатывать события из очереди и затем возвращать управление в нашу задачу. | Функция QCoreApplication::processEvents() будет обрабатывать события из очереди и затем возвращать управление в нашу задачу. | ||
Есть и другая возможность избавиться от блокировки - | Есть и другая возможность избавиться от блокировки - | ||
[http://doc.qt.nokia.com/latest/qeventloop.html QEventLoop] класс. Вызывая QEventLoop::exec() мы повторно запускаем обработку и можем подключить сигнал к слоту QEventLoop::quit() для его завершения. | |||
Например: | Например: | ||
Line 114: | Line 114: | ||
<code> | <code> | ||
QNetworkAccessManager qnam; | QNetworkAccessManager qnam; | ||
QNetworkReply | QNetworkReply *reply = qnam.get(QNetworkRequest(QUrl(…))); | ||
QEventLoop loop; | QEventLoop loop; | ||
QObject::connect(reply, SIGNAL (finished()), & | QObject::connect(reply, SIGNAL (finished()), &loop, SLOT (quit())); | ||
loop.exec(); | loop.exec(); | ||
/ | /* reply has finished, use it */ | ||
</code> | </code> | ||
Line 157: | Line 157: | ||
Не забывайте, что мы попадаем в локальный цикл обработки сообщений когда запускаем '''QDialog::exec()''' (и конечно exec() во всех наследниках '''QDialog''') или '''QMenu::exec()''' и тут возможны те же грабли. | Не забывайте, что мы попадаем в локальный цикл обработки сообщений когда запускаем '''QDialog::exec()''' (и конечно exec() во всех наследниках '''QDialog''') или '''QMenu::exec()''' и тут возможны те же грабли. | ||
Начиная с Qt 4.5 QDialog имеет метод '''QDialog:: | Начиная с Qt 4.5 QDialog имеет метод '''QDialog::open()''', который позволяет показать модальный окно диалога без использования локального цикла обработки сообщений. | ||
<code> | <code> | ||
QObject | QObject *object = new QObject; | ||
object->deleteLater(); | object->deleteLater(); | ||
QDialog dialog; | QDialog dialog; | ||
Line 168: | Line 168: | ||
= Qt классы потоков = | |||
Qt поддерживает работу с потоками на протяжении многих лет (класс QThread был представлен в Qt 2.2, выпущенном 22 сентября 2000 года), и с версии 4.0 поддержка потоков присутствует по умолчанию на всех поддерживаемых платформах (тем не менее, она может быть отключена, подробнее см. [https://doc.qt.io/archives/qt-4.8/fine-tuning-features.html здесь]). Сейчас Qt предлагает несколько классов для работы с потоками,давайте их рассмотрим. | |||
== QThread == | |||
[https://doc.qt.io/archives/qt-4.8/qthread.html QThread] является основным низкоуровневым классом для работы с потоками в Qt. Объект QThread представляет один поток выполнения. Благодаря кросс-платформенной природе Qt классу QThread удаётся скрыть весь платформозависимый код, который необходим для работы потоков в различных операционных системах. | |||
Для того, чтобы запустить некоторый код в отдельном потоке с помощью QThread, мы можем создать дочерний класс и переопределить метод QThread::run(): | |||
<code> | <code> | ||
Line 182: | Line 180: | ||
protected: | protected: | ||
void run() { | void run() { | ||
/ | /* ваша реализация потока тут */ | ||
} | } | ||
}; | }; | ||
</code> | </code> | ||
Затем мы можем использовать | |||
<code> | <code> | ||
Thread | Thread *t = new Thread; | ||
t->start(); // start(), | t->start(); // start(), не run()! | ||
</code> | </code> | ||
чтобы запустить новый поток. Следует отметить, что с Qt 4.4 класс QThread больше не является абстрактным классом, теперь виртуальный метод QThread::run() вместо этого просто делает вызов QThread::exec(), который запускает ''цикл событий потока'' (больше информации об этом далее). | |||
== QRunnable и QThreadPool == | |||
[https://doc.qt.io/archives/qt-4.8/qrunnable.html QRunnable] — это простой абстрактный класс, который может быть использован для запуска задачи в отдельном потоке в духе "запустил и забыл". Всё, что для этого необходимо — создать дочерний класс от QRunnable и реализовать его чисто виртуальный метод run(). | |||
<code> | <code> | ||
Line 206: | Line 203: | ||
public: | public: | ||
void run() { | void run() { | ||
/ | /* ваша реализация runnable тут */ | ||
} | } | ||
}; | }; | ||
</code> | </code> | ||
Чтобы действительно запустить объект QRunnable, используется класс [https://doc.qt.io/archives/qt-4.8/qthreadpool.html QThreadPool], который управляет пулом потоков. Вызовом QThreadPool::start(runnable) мы помещаем QRunnable в очередь запуска QThreadPool; как только поток будет доступен, QRunnable будет захвачен и запущен в этом потоке. У всех Qt-приложений есть глобальный пул потоков, доступ к которому можно получить при помощи вызова QThreadPool::globalInstance(), но любой объект может всегда создать свой частный экземпляр QThreadPool и управлять им явно. | |||
Заметим, что поскольку QRunnable не является дочерним классом QObject, в нём отсутствуют встроенные средства для явного взаимодействия с другими компонентами, и вам придётся реализовывать это вручную, используя низкоуровневые поточные примитивы (например, защищённой мьютексами очереди для сбора результатов и т.п.). | |||
== QtConcurrent == | == QtConcurrent == | ||
[https://doc.qt.io/archives/qt-4.8/threads-qtconcurrent.html QtConcurrent] — высокоуровневый API, построенный над QThreadPool, подходящий для наиболее обычных схем параллельных вычислений: [https://ru.wikipedia.org/wiki/Map_(%D0%BF%D1%80%D0%BE%D0%B3%D1%80%D0%B0%D0%BC%D0%BC%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D0%B5) map], [https://ru.wikipedia.org/wiki/%D0%A1%D0%B2%D1%91%D1%80%D1%82%D0%BA%D0%B0_%D1%81%D0%BF%D0%B8%D1%81%D0%BA%D0%B0 свёртка списка], и [http://en.wikipedia.org/wiki/Filter_(higher-order_function) filter]. Он также предлагает метод QtConcurrent::run(), который может быть использован для простого запуска функции в другом потоке. | |||
В отличие от QThread и QRunnable, для QtConcurrent не требуется использовать низкоуровневые примитивы синхронизации: вместо этого все методы QtConcurrent возвращают объект [https://doc.qt.io/archives/qt-4.8/qfuture.html QFuture], который может быть использован для проверки статуса вычислений (прогресса), чтобы приостановить/продолжить/отменить вычисления и который содержит их ''результаты''. Класс [https://doc.qt.io/archives/qt-4.8/qfuturewatcher.html QFutureWatcher] может быть использован для мониторинга прогресса QFuture и взаимодействия с ним средствами синалов и слотов (заметим, что QFuture, будучи value-based классом, не является наследником QObject). | |||
== Сравнение возможностей == | == Сравнение возможностей == | ||
Line 227: | Line 224: | ||
!QThread | !QThread | ||
!QRunnable | !QRunnable | ||
!QtConcurrent<ref>Исключая QtConcurrent::run, которая реализована с помощью QRunnable | !QtConcurrent<ref>Исключая QtConcurrent::run, которая реализована с помощью QRunnable и, соответственно, перенимает все его плюсы и минусы. | ||
</ref> | </ref> | ||
|- | |- | ||
Line 343: | Line 340: | ||
* an '''automatic connection''' (''the default'') means that if the thread the receiver is living in is the same as the current thread, a direct connection is used; otherwise, a queued connection is used. | * an '''automatic connection''' (''the default'') means that if the thread the receiver is living in is the same as the current thread, a direct connection is used; otherwise, a queued connection is used. | ||
In every case, keep in mind ''the thread the emitting object is living in'' has no importance at all! In case of an automatic connection, Qt looks at the thread that invoked the signal and compares it with the thread the receiver is living in to determine which connection type it has to use. In particular, the ''' | In every case, keep in mind ''the thread the emitting object is living in'' has no importance at all! In case of an automatic connection, Qt looks at the thread that invoked the signal and compares it with the thread the receiver is living in to determine which connection type it has to use. In particular, the '''[http://doc.qt.nokia.com/4.7/threads-qobject.html current Qt documentation] (4.7.1) is simply wrong''' when it states: | ||
''Auto Connection (default) The behavior is the same as the Direct Connection, if the emitter and receiver are in the same thread. The behavior is the same as the Queued Connection, if the emitter and receiver are in different threads.'' | ''Auto Connection (default) The behavior is the same as the Direct Connection, if the emitter and receiver are in the same thread. The behavior is the same as the Queued Connection, if the emitter and receiver are in different threads.'' | ||
Line 365: | Line 362: | ||
Thread thread; | Thread thread; | ||
Object obj; | Object obj; | ||
QObject::connect(& | QObject::connect(&thread, SIGNAL (aSignal()), &obj, SLOT (aSlot())); | ||
thread.start(); | thread.start(); | ||
</code> | </code> | ||
Line 385: | Line 382: | ||
protected: | protected: | ||
void run() { | void run() { | ||
/ | /* … */ | ||
} | } | ||
}; | }; | ||
/ | /* … */ | ||
Thread thread; | Thread thread; | ||
Object obj; | Object obj; | ||
QObject::connect(& | QObject::connect(&obj, SIGNAL (aSignal()), &thread, SLOT (aSlot())); | ||
thread.start(); | thread.start(); | ||
obj.emitSignal(); | obj.emitSignal(); | ||
Line 421: | Line 418: | ||
public slots: | public slots: | ||
void doWork() { | void doWork() { | ||
/ | /* … */ | ||
} | } | ||
}; | }; | ||
/ | /* … */ | ||
QThread thread; | QThread thread; | ||
Worker worker; | Worker worker; | ||
connect(obj, SIGNAL (workReady()), & | connect(obj, SIGNAL (workReady()), &worker, SLOT (doWork())); | ||
worker.moveToThread(& | worker.moveToThread(&thread); | ||
thread.start(); | thread.start(); | ||
</code> | </code> | ||
Line 441: | Line 438: | ||
A good example of such an API is '''address resolution''' (just to show you that we're not talking about 3rd-party crappy API. This is something included in every C library out there), which is the process of taking an host name and converting it into an address. This process involves a query to a (usually remote) system — the Domain Name System, or DNS. While usually the response is almost instantaneous, the remote servers may fail, some packet may get lost, the network connection may broke, and so on; in short, it might take dozens of seconds before we get a reply from our query. | A good example of such an API is '''address resolution''' (just to show you that we're not talking about 3rd-party crappy API. This is something included in every C library out there), which is the process of taking an host name and converting it into an address. This process involves a query to a (usually remote) system — the Domain Name System, or DNS. While usually the response is almost instantaneous, the remote servers may fail, some packet may get lost, the network connection may broke, and so on; in short, it might take dozens of seconds before we get a reply from our query. | ||
The only standard API available on UNIX systems is ''blocking'' (not only the old-fashioned gethostbyname(3), but also the newer and better getservbyname(3) and getaddrinfo(3)). | The only standard API available on UNIX systems is ''blocking'' (not only the old-fashioned gethostbyname(3), but also the newer and better getservbyname(3) and getaddrinfo(3)). [http://doc.qt.nokia.com/latest/qhostinfo.html QHostInfo], the Qt class that handles host name lookups, uses a QThreadPool to enable the queries to run in the background (see [http://qt.gitorious.com/qt/qt/blobs/master/src/network/kernel/qhostinfo.cpp here] ; if thread support is turned off, it switches back to a blocking API). | ||
Other simple examples are '''image loading''' and '''scaling'''. | Other simple examples are '''image loading''' and '''scaling'''. [http://doc.qt.nokia.com/latest/qimagereader.html QImageReader] and [http://doc.qt.nokia.com/latest/qimage.html QImage] only offer blocking methods to read an image from a device, or to scale an image to a different resolution. If you're dealing with very large images, these processes can take up to (tens of) seconds. | ||
== Если вы хотите соответсвия с числом процессоров == | == Если вы хотите соответсвия с числом процессоров == | ||
Line 451: | Line 448: | ||
For instance, consider an application that generates thumbnails from a set of images. A '''thread farm''' of ''n'' threads (that is, a thread pool with a fixed number of threads), one per each CPU available in the system (see also QThread::idealThreadCount() ), can spread the work of scaling down the images into thumbnails on all the threads, effectively gaining an almost linear speedup with the number of the processors (for simplicity's sake, we consider the CPU being the bottleneck). | For instance, consider an application that generates thumbnails from a set of images. A '''thread farm''' of ''n'' threads (that is, a thread pool with a fixed number of threads), one per each CPU available in the system (see also QThread::idealThreadCount() ), can spread the work of scaling down the images into thumbnails on all the threads, effectively gaining an almost linear speedup with the number of the processors (for simplicity's sake, we consider the CPU being the bottleneck). | ||
== Если вы не хотите быть | == Если вы не хотите быть заблокированными другими == | ||
MEH. BETTER START WITH AN EXAMPLE. | MEH. BETTER START WITH AN EXAMPLE. | ||
Line 512: | Line 509: | ||
</code> | </code> | ||
A much '''better and simpler way''' of achieving the same result is simply using timers, i.e. a | A much '''better and simpler way''' of achieving the same result is simply using timers, i.e. a [http://doc.qt.nokia.com/latest/qtimer.html QTimer] object with a 1s timeout, and make the doWork() method a slot: | ||
<code> | <code> | ||
Line 521: | Line 518: | ||
public: | public: | ||
Worker() { | Worker() { | ||
connect(& | connect(&timer, SIGNAL (timeout()), this, SLOT (doWork())); | ||
timer.start(1000); | timer.start(1000); | ||
} | } | ||
Line 539: | Line 536: | ||
== Сеть / Конечные автоматы == | |||
A very common design pattern when dealing with network operations is the following one: | A very common design pattern when dealing with network operations is the following one: | ||
Line 558: | Line 554: | ||
socket->write(reply); | socket->write(reply); | ||
socket->waitForBytesWritten(); | socket->waitForBytesWritten(); | ||
/ | /* … and so on … */ | ||
</code> | </code> | ||
Needless to say, the various waitFor | Needless to say, the various waitFor*() calls block the caller without returning to the event loop, freezing the UI and so on. Notice that the above snippet does not take into account any error handling, otherwise it would have been even more cumbersome. What is very wrong in this design is that we're forgetting that '''networking is asynchronous by design''', and if we build a synchronous processing around we're shooting ourselves in the foot. To solve this problem, many people simple move this code into a different thread. | ||
Another more abstract example: | Another more abstract example: | ||
Line 590: | Line 586: | ||
and so forth. | and so forth. | ||
Now, there are several ways to build a state machine (and Qt even offers a class for that: | Now, there are several ways to build a state machine (and Qt even offers a class for that: [http://doc.qt.nokia.com/4.7/qstatemachine.html QStateMachine] ), the simplest one being an enum (i.e. an integer) used to remember the current state. We can rewrite the above snippets like this: | ||
<code> | <code> | ||
Line 613: | Line 609: | ||
switch (state) { | switch (state) { | ||
case State1: | case State1: | ||
/ | /* … */ | ||
state = State2; | state = State2; | ||
break; | break; | ||
case State2: | case State2: | ||
/ | /* … */ | ||
state = State3; | state = State3; | ||
break; | break; | ||
/ | /* etc. */ | ||
} | } | ||
} | } |
Latest revision as of 09:28, 13 November 2020
This article may require cleanup to meet the Qt Wiki's quality standards. Reason: Auto-imported from ExpressionEngine. Please improve this article if you can. Remove the {{cleanup}} tag and add this page to Updated pages list after it's clean. |
Русский English 中文 한국어 Български
p{font-weight:bold;font-size:18px;}. [DRAFT]
Потоки, События и QObjects
(или: вы делаете это неправильно)
Внимание: Beta версия
Статья находится на стадии завершения, однако требует некоторой полировки и добавления хороших примеров. Обзор и сотрудничество приветствуются. Дискуссия по этой статье находится здесь. Дискуссия по русскому переводу находится здесь.
Вступление
Одна из наиболее популярных тем на "#qt IRC channel":irc://irc.freenode.net/#qt это потоки. Множество людей заходят на канал и спрашивают, как им решить проблему с некоторым кодом, выполняющемся в другом потоке.
В девяти из десяти случаев, беглый осмотр их кода показывает, что наибольшая проблема состоит в том факте, что они используют потоки в первый раз и попадают в бесконечные ловушки параллельного программирования.
Легкость создания и запуска потоков в Qt, в сочетании с некоторым незнанием стилей программирования (особенно асинхронного сетевого программирования, в сочетании с Qt–архитектурой сигналов и слотов) и/или привычки, приобретенные при использовании других инструментариев или языков, обычно приводят к тому, что люди выстреливают себе в ногу. Кроме того, поддержка потоков в Qt - это палка о двух концах: в то время как это делает создание многопоточных приложений очень простым для вас, это добавляет определенное количество особенностей (особенно когда дело доходит до взаимодействия с QObject), о которых вы должны знать.
Целью данного документа не является научить вас использовать потоки, делать правильное блокирование, использовать параллельность и писать масштабируемые программы; есть много хороших книг на эти темы; например, взгляните на список рекомендованного чтения на этой странице. Вместо этого, эта небольшая заметка предназначена для введения пользователей в потоки Qt 4, для того чтобы избежать наиболее распространенных ошибок и помочь им разрабатывать код, одновременно и более надежный, и имеющий лучшую структуру.
Предпосылки
Введение общего назначения в программирование (потоков) отсутствует, мы считаем, что вы уже обладаете некоторыми знаниями об:
- Основы C++ (хотя большая часть рекомендаций также подойдет и для других языков);
- Основы Qt: QObjects, сигналы и слоты, обработка событий;
- Что такое поток, и какие существуют отношения между потоками, процессами и операционной системой;
- Как запустить и остановить поток, или подождать пока он завершиться, (по крайней мере) под одной известной ОС.;
- Как использовать мьютексы, семафоры и ожидать условия для создания потоко–безопасных/reentrant функций, структур, классов.
В этом документе мы будем следовать терминологии Qt, В которой сказано:
- Реентерабельный Класс является реентерабельным, если безопасно использовать его экземпляры более чем из одного потока, при условии, что не более одного потока имеют доступ к экземпляру одновременно. Функция является реентерабельной, если безопасно вызывать ее из более чем одного потока одновременно, при условии, что каждый вызов ссылается на уникальные данные. Другими словами, это означает, что пользователи этого класса/функции должны сериализовать все попытки доступа к экземплярам/общим данным средствами некоторого внешнего механизма блокировки.
- Потокобезопасный Класс является потокобезопасным, если безопасно использовать его экземпляры из более чем одного потока одновременно. Функция является потокобезопасной, если безопасно вызывать ее из более чем одного потока одновременно, даже если вызовы ссылаются на общие данные.
События и цикл обработки событий
События и доставка событий играют центральную роль в архитектуре Qt. В этой статье мы не рассматриваем эту тему досконально; вместо этого внимание уделяется некоторым связанным с потоками ключевым концепциям (здесь и здесь можно найти больше информации по событиям в Qt).
Событием в Qt называется объект, представляющий что–то интересное из произошедшего; главным отличием событий от сигналов является то, что события предназначены для конкретного объекта в нашем приложении (который решает, что с этим событием делать), а сигналы "гуляют сами по себе". С точки зрения кода все события являются объектами какого–либо подкласса QEvent, и все производные от Object классы могут переопределять виртуальный метод QObject::event() для работы с событиями, предназначенными для данного объекта.
События могут быть сгенерированы как внутри, так и снаружи приложения, например:
- Объекты QKeyEvent и QMouseEvent представляют взаимодействие с помощью клавиатуры или мыши и приходят из оконного менеджера;
- Объекты QTimerEvent приходят в QObject, когда один из таймеров срабатывает, они (как правило) приходят из операционной системы;
- Объекты QChildEvent приходят в QObject, когда добавляется или удаляется потомок, они приходят из вашего приложения.
Важным моментом является то, что события приходят не как только они были сгенерированы; вместо этого они попадают в очередь событий и приходят позже. Диспетчер циклически обрабатывает очередь событий и отправляет события по месту назначения, поэтому это называется циклом обработки событий. Концептуально цикл обработки событий выглядит так (см. статью в Qt Quarterly по ссылке выше):
while (is_active)
{
while (!event_queue_is_empty)
dispatch_next_event();
wait_for_more_events();
}
Мы входми в главный цикл обработки событий Qt при выполнении команды QCoreApplication::exec(); этот вызов блокируется пока QCoreApplication::exit() или QCoreApplication::quit() не будут вызваны, и не завершат цикл обработки событий.
Функция "wait_for_more_events()" блокируется (в том смысле, что это не занимает ожидания) пока некоторое событие не будет сгенерировано. Если задуматься, все что может генерировать события в этой точке, это некоторые внешние источники (рассылка всех внутренних событий к этому моменту завершена и в очереди больше нет событий ожидающих своей доставки). Таким образом цикл обработки событий может быть "разбужен":
- Оконным менеджером (нажатие клавиш/мыши, взаимодействиям с окнами, и т. д.);
- Сокетами (если доступны новые данные для чтения,или сокет готов к записи без блокировки, есть новые входящие соединения и т. д.);
- Таймеры (если таймер сработал);
- События посланные из других потоков (смотри дальше).
В UNIX-подобных системах, менеджер окон (то есть X11) уведомляет приложения через сокеты (Unix Domain или TCP/IP), так как клиенты используют их для общения с X сервером. Если мы решим осуществить межпоточные вызовы событий с внутренними socketpair(2), все что осталось чтобы пробудить цикл обработки событий:
- Сокеты;
- Таймеры;
что является именно тем, что делает системный вызов select(2): он следит с помощью набора дескрипторов за активностью и срабатывает (с настраиваемым временем ожидания), если нет никакой активности в течение определенного времени. Все что нужно сделать Qt это преобразовать то, что возвращает select в объект прямого потомка QEvent и добавить его в очередь событий. Теперь вы знаете, что находится внутри цикла обработки событий :)
Что требует выполняющегося цикла обработки событий?
Это не исчерпывающий список, но имея общую картину, вы сможете догадаться какие классы действительно требуют выполнения цикла обработки событий.
- Отрисовка виджетов и взаимодействие: QWidget::paintEvent() будет вызвано при доставке объектов QPaintEvent, которые генерируются и вызовом QWidget::update() (т.е. внутренне), и оконным менеджером (например потому, что скрытое окно было показано). То же самое справедливо для всех видов взаимодействия (клавиатура, мышь и т.д.): необходимо, чтобы соответствующие события были добавлены в цикл обработки событий.
- Таймеры: Короче говоря, они активизируются, когда срабатывает select(2) или подобный вызов, таким образом, вы должны позволить Qt сделать эти вызовы для вас, путем возвращения в цикл обработки событий.
- Сеть: все низкоуровневые классы Qt для работы с сетью (QTcpSocket, QUdpSocket, QTcpServer и т.д.) спроектированы асинхронными. Когда вы вызываете read(), они возвращают только уже доступные данные; когда вы вызываете write(), они добавляют задачу в список для записи позже. Фактическое чтение/запись происходят только когда вы возвращаетесь в цикл обработки событий. Обратите внимание, что они действительно предлагают синхронные методы (семейство методов waitFor*), но их использовать не рекомендуется, так как они блокируют цикл событий во время ожидания. Классы высокого уровня, типа QNetworkAccessManager, просто не предоставляют синхронного API и требуют наличия цикла событий.
Блокирование цикла обработки событий
Перед обсуждением почему вы никогда не должны блокировать цикл обработки событий, давайте попытаемся выяснить, а что такое "блокировка". Предположим что у вас есть кнопка, которая испускает сигнал соединенный со слотов нашего рабочего объекта, который делает очень много работы. После нажатия на кнопку стек вызовов будет выглядеть следующим образом (стек растет вниз):
- main(int, char *)
- QApplication::exec()
- […]
- QWidget::event(QEvent *)
- Button::mousePressEvent(QMouseEvent*)
- Button::clicked()
- […]
- Worker::doWork()
В main() мы запускаем цикл обработки событий, как обычно, вызывая QApplication::exec() (строка 2). Оконный менеджер прислал нам событие нажатия на кнопку мыши, которое бло захвачено ядром Qt, преобразовано в QMouseEvent и послано методу нашего виджета event() (строка 4) от QApplication::notify() (не показан). Так как event() не переопределен у Button, была вызвана реализация базового класса (QWidget). QWidget::event() определил событие как нажатие мыши и вызвал специализированный обработчик Button::mousePressEvent() (строка 5). Мы переопределили этот метод для испускания сигнала Button::clicked() (строка 6), который вызывает слот Worker::doWork в нашем рабочем объекте (строка 7).
Пока рабочий объект занят работой, что делает цикл обработки событий? Вы должны были догадаться: ничего! Он отправил нажатие кнопки мыши и ждет в блокировке пока обработчик события вернет управление. Нам удалось блокировать цикл обработки событий, что означает что никакие события больше не могут посылаться, пока мы не вернемся из слота doWork(), вверх по стеку в цикл обработки событий и не позволим ему обработать ожидающие события.
При остановленной доставке событий, виджеты не будут обновлять себя (объекты QPaintEvent будут сидеть в очереди), невозможно дальнейшее взаимодествие с виджетами (по той же причине), таймеры не будут срабатывать и сетевые коммуникации будут замедлены и остановлены. Кроме того, многие оконные менеджеры обнаружат, что ваше приложение более не обрабатывает события и сообщат пользователю, что ваше приложение не отвечает. Вот почему так важно быстро реагировать на события и возвращаться к циклу обработки как можно скорее!
Принудительная обработка событий.
Так что же делать, если нам необходимо выполнить длительную операцию при этом избежать блокировки очереди (цикла) сообщений? Во-первых, мы можем переместить задачу в другой поток, ниже мы рассмотрим этот вариант подробней. Во-вторых, мы имеем возможность вручную запускать процесс обработки событий периодически, вызывая QCoreApplication::processEvents() внутри блокируещй задачи. Функция QCoreApplication::processEvents() будет обрабатывать события из очереди и затем возвращать управление в нашу задачу.
Есть и другая возможность избавиться от блокировки - QEventLoop класс. Вызывая QEventLoop::exec() мы повторно запускаем обработку и можем подключить сигнал к слоту QEventLoop::quit() для его завершения.
Например:
QNetworkAccessManager qnam;
QNetworkReply *reply = qnam.get(QNetworkRequest(QUrl(…)));
QEventLoop loop;
QObject::connect(reply, SIGNAL (finished()), &loop, SLOT (quit()));
loop.exec();
/* reply has finished, use it */
QNetworkReply не предоставляет блокирующего (синхронного) API, ему необходимо чтобы очередь сообщений работала. Мы входим (блокируемся при вызове) в локальную очередь обработки loop.exec() и только когда сетевой запрос выполнится, локальная очередь завершится благодаря слоту quit().
Будьте очень осторожны с циклом обработки очереди сообщений, неправильное использование может привести к нежелательной рекурсии. Давайте вернемся к примеру с кнопкой. Если мы вызовем QCoreApplication::processEvents() внутри слота doWork(), и пользователь нажмет кнопку еще раз, слот doWork() будет вызыван еще раз.
- main(int, char)
- QApplication::exec()
- […]
- QWidget::event(QEvent )
- Button::mousePressEvent(QMouseEvent)
- Button::clicked()
- […]
- Worker::doWork() // первый вызов
- QCoreApplication::processEvents() // мы вручную вызвали обработку события, и…
- […]
- QWidget::event(QEvent *) // еще один клик по кнопке…
- Button::mousePressEvent(QMouseEvent *)
- Button::clicked() // который излучает click() еще раз…
- […]
- Worker::doWork() // БАЦ! мы рекурсивно попали в наш слот
Простой и быстрый хак в этом случае - передача QEventLoop::ExcludeUserInputEvents в функцию QCoreApplication::processEvents() в качестве параметра, который говорит циклу обработки игнорировать любой пользовательский ввод (такие события останутся лежать в очереди).
Кроме того, проблемы могут возникнуть с deletion events помещенными в очередь вызовом deleteLater() или, в общем случае, с любыми событиями, вызывающими удаление объекта.
QObject *object = new QObject;
object->deleteLater();
QEventLoop loop;
loop.exec();
/*Теперь указатель object невалидный*/
Удаление обьектов не происходит в QCoreApplication::processEvents() (начиная с Qt 4.3 этот метод не обрабатывает deletion event-ы), но локальный обработчик очереди сообщений обрабатывает эти события и удаляет объекты.
Не забывайте, что мы попадаем в локальный цикл обработки сообщений когда запускаем QDialog::exec() (и конечно exec() во всех наследниках QDialog) или QMenu::exec() и тут возможны те же грабли. Начиная с Qt 4.5 QDialog имеет метод QDialog::open(), который позволяет показать модальный окно диалога без использования локального цикла обработки сообщений.
QObject *object = new QObject;
object->deleteLater();
QDialog dialog;
dialog.exec();
/*Теперь указатель object невалидный*/
Qt классы потоков
Qt поддерживает работу с потоками на протяжении многих лет (класс QThread был представлен в Qt 2.2, выпущенном 22 сентября 2000 года), и с версии 4.0 поддержка потоков присутствует по умолчанию на всех поддерживаемых платформах (тем не менее, она может быть отключена, подробнее см. здесь). Сейчас Qt предлагает несколько классов для работы с потоками,давайте их рассмотрим.
QThread
QThread является основным низкоуровневым классом для работы с потоками в Qt. Объект QThread представляет один поток выполнения. Благодаря кросс-платформенной природе Qt классу QThread удаётся скрыть весь платформозависимый код, который необходим для работы потоков в различных операционных системах.
Для того, чтобы запустить некоторый код в отдельном потоке с помощью QThread, мы можем создать дочерний класс и переопределить метод QThread::run():
class Thread : public QThread {
protected:
void run() {
/* ваша реализация потока тут */
}
};
Затем мы можем использовать
Thread *t = new Thread;
t->start(); // start(), не run()!
чтобы запустить новый поток. Следует отметить, что с Qt 4.4 класс QThread больше не является абстрактным классом, теперь виртуальный метод QThread::run() вместо этого просто делает вызов QThread::exec(), который запускает цикл событий потока (больше информации об этом далее).
QRunnable и QThreadPool
QRunnable — это простой абстрактный класс, который может быть использован для запуска задачи в отдельном потоке в духе "запустил и забыл". Всё, что для этого необходимо — создать дочерний класс от QRunnable и реализовать его чисто виртуальный метод run().
class Task : public QRunnable {
public:
void run() {
/* ваша реализация runnable тут */
}
};
Чтобы действительно запустить объект QRunnable, используется класс QThreadPool, который управляет пулом потоков. Вызовом QThreadPool::start(runnable) мы помещаем QRunnable в очередь запуска QThreadPool; как только поток будет доступен, QRunnable будет захвачен и запущен в этом потоке. У всех Qt-приложений есть глобальный пул потоков, доступ к которому можно получить при помощи вызова QThreadPool::globalInstance(), но любой объект может всегда создать свой частный экземпляр QThreadPool и управлять им явно.
Заметим, что поскольку QRunnable не является дочерним классом QObject, в нём отсутствуют встроенные средства для явного взаимодействия с другими компонентами, и вам придётся реализовывать это вручную, используя низкоуровневые поточные примитивы (например, защищённой мьютексами очереди для сбора результатов и т.п.).
QtConcurrent
QtConcurrent — высокоуровневый API, построенный над QThreadPool, подходящий для наиболее обычных схем параллельных вычислений: map, свёртка списка, и filter. Он также предлагает метод QtConcurrent::run(), который может быть использован для простого запуска функции в другом потоке.
В отличие от QThread и QRunnable, для QtConcurrent не требуется использовать низкоуровневые примитивы синхронизации: вместо этого все методы QtConcurrent возвращают объект QFuture, который может быть использован для проверки статуса вычислений (прогресса), чтобы приостановить/продолжить/отменить вычисления и который содержит их результаты. Класс QFutureWatcher может быть использован для мониторинга прогресса QFuture и взаимодействия с ним средствами синалов и слотов (заметим, что QFuture, будучи value-based классом, не является наследником QObject).
Сравнение возможностей
QThread | QRunnable | QtConcurrent[1] | |
---|---|---|---|
Высокоуровневый API | ✘ | ✘ | ✔ |
Ориентированный на задачу | ✘ | ✔ | ✔ |
Встроенная поддержка паузы/возобновления/отмены | ✘ | ✘ | ✔ |
Может быть запущен с различным приоритетом | ✔ | ✘ | ✘ |
Может запустить цикл обработки событий | ✔ | ✘ | ✘ |
Потоки и QObjects
Per-thread event loop
So far we've always talked about "the event loop", taking somehow per granted that there's only one event loop in a Qt application. This is not the case: QThread objects can start thread-local event loops running in the threads they represent. Therefore, we say that the main event loop is the one created by the thread which invoked main(), and started with QCoreApplication::exec() (which must be called from that thread). This is also called the GUI thread, because it's the only thread in which GUI-related operations are allowed. A QThread local event loop can be started instead by calling QThread::exec() (inside its run() method):
class Thread : public QThread {
protected:
void run() {
/* … initialize … */
exec();
}
};
As we mentioned before, since Qt 4.4 QThread::run() is no longer a pure virtual method; instead, it calls QThread::exec(). Exactly like QCoreApplication, QThread has also the QThread::quit() and QThread::exit() methods to stop the event loop.
A thread event loop delivers events for all QObjects that are living in that thread; this includes, by default, all objects that are created into that thread, or that were moved to that thread (more info about this later). We also say that the thread affinity of a QObject is a certain thread, meaning that the object is living in that thread. We can query anytime the thread affinity of a QObject by calling QObject::thread(). This applies to objects which are built in the constructor of a QThread object:
class MyThread : public QThread
{
public:
MyThread()
{
otherObj = new QObject;
}
private:
QObject obj;
QObject *otherObj;
QScopedPointer<QObject> yetAnotherObj;
};
What's the thread affinity of obj, otherObj, yetAnotherObj after we create a MyThread object? We must look at the thread that created them: it's the thread that ran the MyThread constructor. Therefore, all three objects are not living in the MyThread thread, but in the thread that created the MyThread instance (which, by the way, is where the instance is living as well).
Notice that QObjects created before a QCoreApplication object have no thread affinity, and therefore no event dispatching will be done for them (in other words, QCoreApplication builds up the QThread object that represents the main thread).
http://doc.qt.nokia.com/4.7/images/threadsandobjects.png
We can use the thread-safe QCoreApplication::postEvent() method for posting an event for a certain object. This will enqueue the event in the event loop of the thread the object is living in; therefore, the event will not be dispatched unless that thread has a running event loop.
It is very important to understand that QObject and all of its subclasses are not thread-safe (although they can be reentrant); therefore, you can not access a QObject from more than one thread at the same time, unless you serialize all accesses to the object's internal data (for instance, by protecting it with a mutex). Remember that the object may be handling events dispatched by the event loop of the thread it is living in while you're accessing it from another thread! For the same reason, you can't delete a QObject from another thread, but you must use QObject::deleteLater(), which will post an event that will ultimately cause its deletion by the thread the object is living in.
Moreover, QWidget and all of its subclasses, along with other GUI-related classes (even not QObject-based, like QPixmap) are not reentrant either: they can be used exclusively from the GUI thread.
We can change a QObject's affinity by calling QObject::moveToThread(); this will change the affinity of the object and of its children. Since QObject is not thread-safe, we must use it from the thread the object is living in; that is, you can only push objects from the thread they're living in to other threads, and not pull them or move them around from other threads. Moreover, Qt requires that the child of a QObject must live in the same thread where the parent is living. This implies that:
- you can't use QObject::moveToThread() on a object which has a parent;
- you must not create objects in a QThread using the QThread object itself as their parent:
class Thread : public QThread {
void run() {
QObject obj = new QObject(this); // WRONG[[Image:|Image:]]!
}
};
This is because the QThread object is living in another thread, namely, the one in which it was created.
Qt also requires that all objects living in a thread are deleted before the QThread object that represents the thread is destroyed; this can be easily done by creating all the objects living in that thread on the QThread::run() method's stack.
Сигналы из слоты из потока в поток
Given these premises, how do we call methods on QObjects living in other threads? Qt offers a very nice and clean solution: we post an event in that thread's event queue, and the handling of that event will consist in invoking the method we're interested in (this of course requires that the thread has a running event loop). This facility is built around the method introspection provided by moc: therefore, only signals, slots and methods marked with the Q_INVOKABLE macro are invokable from other threads.
The QMetaObject::invokeMethod() static method does all the work for us:
QMetaObject::invokeMethod(object, "methodName",
Qt::QueuedConnection,
Q_ARG(type1, arg1),
Q_ARG(type2, arg2));
Notice that since the arguments need to be copied in the event which is built behind the scenes, their types need to provide a public constructor, a public destructor and a public copy constructor, and must be registered within Qt type system by using the qRegisterMetaType() function.
Signals and slots across threads work in a similar way. When we connect a signal to a slot, the fifth argument of QObject::connect is used to specify the connection type:
- a direct connection means that the slot is always invoked directly by the thread the signal is emitted from;
- a queued connection means that an event is posted in the event queue of the thread the receiver is living in, which will be picked up by the event loop and will cause the slot invocation sometime later;
- a blocking queued connection is like a queued connection, but the sender thread blocks until the event is picked up by the event loop of the thread the receiver is living in, the slot is invoked, and it returns;
- an automatic connection (the default) means that if the thread the receiver is living in is the same as the current thread, a direct connection is used; otherwise, a queued connection is used.
In every case, keep in mind the thread the emitting object is living in has no importance at all! In case of an automatic connection, Qt looks at the thread that invoked the signal and compares it with the thread the receiver is living in to determine which connection type it has to use. In particular, the current Qt documentation (4.7.1) is simply wrong when it states:
Auto Connection (default) The behavior is the same as the Direct Connection, if the emitter and receiver are in the same thread. The behavior is the same as the Queued Connection, if the emitter and receiver are in different threads.
because the emitter object's thread affinity does not matter. For instance:
class Thread : public QThread
{
Q_OBJECT
signals:
void aSignal();
protected:
void run() {
emit aSignal();
}
};
/* … */
Thread thread;
Object obj;
QObject::connect(&thread, SIGNAL (aSignal()), &obj, SLOT (aSlot()));
thread.start();
The signal aSignal() will be emitted by the new thread (represented by the Thread object); since it is not the thread the Object object is living in (which, by the way, is the same thread the Thread object is living in, just to stress that the sender's thread affinity doesn't matter), a queued connection will be used.
Another common pitfall is the following one:
class Thread : public QThread
{
Q_OBJECT
slots:
void aSlot() {
/* … */
}
protected:
void run() {
/* … */
}
};
/* … */
Thread thread;
Object obj;
QObject::connect(&obj, SIGNAL (aSignal()), &thread, SLOT (aSlot()));
thread.start();
obj.emitSignal();
When "obj" emits its aSignal() signal, which kind of connection will be used? You should've guessed it: a direct connection. That's because the Thread object is living in the thread that emits the signal. In the aSlot() slot we could then access some Thread's member variable while they're being accessed by the run() method, which is running concurrently: this is the perfect recipe for disaster. A solution you'll often found in forums, blog posts etc. is to add a moveToThread(this) to the Thread constructor:
class Thread : public QThread {
Q_OBJECT
public:
Thread() {
moveToThread(this); // WRONG
}
/* … */
};
which indeed will work (because now the affinity of the Thread object changed), but it's a very bad design. What's wrong here is that we're misunderstanding the purpose of a thread object (the QThread subclass): QThread objects are not threads; they're control objects around a thread, therefore meant to be used from another thread (usually, the one they're living in).
A good way to achieve the same result is splitting the "working" part from the "controller" part, that is, writing a QObject subclass and using QObject::moveToThread() to change its affinity:
class Worker : public QObject
{
Q_OBJECT
public slots:
void doWork() {
/* … */
}
};
/* … */
QThread thread;
Worker worker;
connect(obj, SIGNAL (workReady()), &worker, SLOT (doWork()));
worker.moveToThread(&thread);
thread.start();
Когда я должен использовать потоки?
Когда вы должны использовать блокировочный API
If you need to use a library or other code that doesn't offer a non-blocking API (by means of signals and slots, or events, or callbacks, etc.), then the only viable solution in order to avoid freezing the event loop is to spawn a process or a thread. Since creating a new worker process, having it doing the job and communicating back the results is definetely harder and more expensive than just starting a thread, the latter is the most common choice.
A good example of such an API is address resolution (just to show you that we're not talking about 3rd-party crappy API. This is something included in every C library out there), which is the process of taking an host name and converting it into an address. This process involves a query to a (usually remote) system — the Domain Name System, or DNS. While usually the response is almost instantaneous, the remote servers may fail, some packet may get lost, the network connection may broke, and so on; in short, it might take dozens of seconds before we get a reply from our query.
The only standard API available on UNIX systems is blocking (not only the old-fashioned gethostbyname(3), but also the newer and better getservbyname(3) and getaddrinfo(3)). QHostInfo, the Qt class that handles host name lookups, uses a QThreadPool to enable the queries to run in the background (see here ; if thread support is turned off, it switches back to a blocking API).
Other simple examples are image loading and scaling. QImageReader and QImage only offer blocking methods to read an image from a device, or to scale an image to a different resolution. If you're dealing with very large images, these processes can take up to (tens of) seconds.
Если вы хотите соответсвия с числом процессоров
Threads allow your program to take advantage from multiprocessor systems. Since each thread is scheduled independently by the operating system, if your application is running on such a machine the scheduler is likely to run each thread on a different processor at the same time.
For instance, consider an application that generates thumbnails from a set of images. A thread farm of n threads (that is, a thread pool with a fixed number of threads), one per each CPU available in the system (see also QThread::idealThreadCount() ), can spread the work of scaling down the images into thumbnails on all the threads, effectively gaining an almost linear speedup with the number of the processors (for simplicity's sake, we consider the CPU being the bottleneck).
Если вы не хотите быть заблокированными другими
MEH. BETTER START WITH AN EXAMPLE.
This is quite an advanced topic, so feel free to skip it for now. A nice example of this use case comes from QNetworkAccessManager usage inside WebKit. WebKit is a modern browser engine, that is, a set of classes to lay out and display web pages. The Qt widget that uses WebKit is QWebView.
QNetworkAccessManager is a Qt class that deals with HTTP requests and responses for all purposes, we can consider it to be the networking engine of a web browser. Its current design does not make use of any worker threads; all networking is handled in the same thread QNetworkAccessManager and its QNetworkReplys are living in.
While not using threads for networking is a very good idea, it has also a major drawback: if you don't read data from the socket as soon as possible, the kernel buffers will fill up, packets will begin to be dropped, and the transfer speed will decrease considerably.
Socket activity (i.e., availability of some data to read from a socket) is managed by Qt's event loop. Blocking the event loop will therefore lead to a loss of transfer performance, because nobody will be notified that there are data to read (and thus nobody will read them).
But what could block the event loop? The sad answer is: WebKit itself! As soon as some data are received, WebKit uses them to start laying out the web page. Unfortunately, the layout process is quite complicated and expensive, therefore it blocks the event loop for a (short) while, enough to impact on ongoing transfers (broadband connections play their role here, filling up kernel buffers in a small fraction of second).
To sum it up, what happens is something like this:
- WebKit issues a request;
- some data from the reply begin to arrive;
- WebKit starts to lay out the web page using the incoming data, blocking the event loop;
- without a running event loop, data are received by the OS, but not read from QNetworkAccessManager sockets;
- kernel buffers will fill up, and the transfer will slow down.
The overall page loading time is therefore worsened by this self-induced transfer slowness.
The engineers at Nokia are experimenting with a threaded QNetworkAccessManager, which should solve this kind of problems. Notice that since QNetworkAccessManagers and QNetworkReplys are QObjects, they're not thread-safe, therefore you can't just move them to another thread and continue using them from your thread, because they may be accessed at the same time by two threads: yours and the one they're living in, due to events that will be dispatched to them by the latter thread's event loop.
Если вы хотите делать вещи типа: pr0 h4x0r 31337
Зачем тогда вы читаете это, вообще?
Когда я не должен использовать потоки?
Таймеры
This is perhaps the worst form of thread abuse. If we have to invoke a method repeatedly (for instance, every second), many people end up with something like this:
// VERY WRONG
while (condition) {
doWork();
sleep(1); // this is sleep(3) from the C library
}
Then they figure out that this is blocking the event loop, therefore decide to bring in threads:
// WRONG
class Thread : public QThread {
protected:
void run() {
while (condition) {
// notice that "condition" may also need volatiness and mutex protection
// if we modify it from other threads (!)
doWork();
sleep(1); // this is QThread::sleep()
}
}
};
A much better and simpler way of achieving the same result is simply using timers, i.e. a QTimer object with a 1s timeout, and make the doWork() method a slot:
class Worker : public QObject
{
Q_OBJECT
public:
Worker() {
connect(&timer, SIGNAL (timeout()), this, SLOT (doWork()));
timer.start(1000);
}
private slots:
void doWork() {
/* … */
}
private:
QTimer timer;
};
All we need is a running event loop, then the doWork() method will be invoked each second.
Сеть / Конечные автоматы
A very common design pattern when dealing with network operations is the following one:
socket->connect(host);
socket->waitForConnected();
data = getData();
socket->write(data);
socket->waitForBytesWritten();
socket->waitForReadyRead();
socket->read(response);
reply = process(response);
socket->write(reply);
socket->waitForBytesWritten();
/* … and so on … */
Needless to say, the various waitFor*() calls block the caller without returning to the event loop, freezing the UI and so on. Notice that the above snippet does not take into account any error handling, otherwise it would have been even more cumbersome. What is very wrong in this design is that we're forgetting that networking is asynchronous by design, and if we build a synchronous processing around we're shooting ourselves in the foot. To solve this problem, many people simple move this code into a different thread.
Another more abstract example:
result = process_one_thing();
if (result->something())
process_this();
else
process_that();
wait_for_user_input();
input = read_user_input();
process_user_input(input);
/* … */
Which has more or less the same pitfalls of the networking example.
Let's take a step back and consider from an higher point of view what we're building here: we want to create a state machine that reacts on inputs of some sort and acts consequently. For instance, with the networking example, we might want to build something like this:
- Idle → Connecting (when calling connectToHost());
- Connecting → Connected (when connected() is emitted);
- Connected → LoginDataSent (when we send the login data to the server);
- LoginDataSent → LoggedIn (the server replied with an ACK)
- LoginDataSent → LoginError (the server replied with a NACK)
and so forth.
Now, there are several ways to build a state machine (and Qt even offers a class for that: QStateMachine ), the simplest one being an enum (i.e. an integer) used to remember the current state. We can rewrite the above snippets like this:
class Object : public QObject
{
Q_OBJECT
enum State {
State1, State2, State3 /* and so on */
};
State state;
public:
Object() : state(State1)
{
connect(source, SIGNAL (ready()), this, SLOT (doWork()));
}
private slots:
void doWork() {
switch (state) {
case State1:
/* … */
state = State2;
break;
case State2:
/* … */
state = State3;
break;
/* etc. */
}
}
};
What the "source" object and its "ready()" signal are? Exactly what we want them to be: for instance, in the networking example, we might want to connect the socket's QAbstractSocket::connected() and the QIODevice::readyRead() signals to our slot. Of course, we can also easily add more slots if that suits better in our case (like a slot to manage error situations, which are notified by the QAbstractSocket::error() signal). This is a true asynchronous, signal-driven design!
Расщепление задач на части
Suppose that we have a long computation which can't be easily moved to another thread (or that it can't be moved at all, because for instance it must run in the GUI thread). If the we can split the computation in small chunks, we can return to the event loop, let it dispatch events, and make it invoke the method that processes the next chunk. This can be easily done if we remember how queued connections are implemented: an event is posted in the event loop of the thread the receiver object is living in; when the event is delivered, the corresponding slot is invoked.
We can use QMetaObject::invokeMethod() to achieve the same result by specifying Qt::QueuedConnection as the type of the invocation; this just requires the method to be invocable, therefore it must be either a slot or marked with the Q_INVOKABLE macro. If we also want to pass parameters to the method, they need to be registered within the Qt metatype system using qRegisterMetaType(). The following snippet shows this pattern:
class Worker : public QObject
{
Q_OBJECT
public slots:
void startProcessing()
{
processItem(0);
}
void processItem(int index)
{
/* process items[index] … */
if (index < numberOfItems)
QMetaObject::invokeMethod(this,
"processItem",
Qt::QueuedConnection,
Q_ARG(int, index + 1));
}
};
Since there are no threads involved, it's easy to pause/resume/cancel such a computation and collect the results back.
Примеры
MD5 хеш
- ↑ Исключая QtConcurrent::run, которая реализована с помощью QRunnable и, соответственно, перенимает все его плюсы и минусы.