Потоки и QObjects
QThread наследует QObject. Он излучает сигналы для указания того, что поток начал или закончил выполнение, а также предоставляет несколько слотов.
Более интересно то, что QObject можно использовать в нескольких потоках, излучать сигналы, которые вызывают слоты в других потоках, и публиковать события для объектов, «обитающих» в других потоках. Это возможно, потому что каждый поток может иметь свою собственную очередь событий.
Взаимоисключаемость QObject
QObject является реентерабельным. Большинство его подклассов, не относящихся к графическому интерфейсу, таких как QTimer, QTcpSocket, QUdpSocket и QProcess, также являются реентерабельными, что позволяет использовать эти классы одновременно из нескольких потоков. Обратите внимание, что эти классы предназначены для создания и использования в рамках одного потока; создание объекта в одном потоке и вызов его функций из другого потока не гарантируется. Существует три ограничения, о которых нужно знать:
-
Потомок QObject всегда должен создаваться в том же потоке, где был создан родитель. Это подразумевает, в частности, что вы никогда не должны передавать объект QThread (
this) в качестве родителя объекта, созданного в потоке (поскольку сам объект QThread был создан в другом потоке). - Объекты, управляемые событиями, могут использоваться только в одном потоке. В частности, это относится к механизму таймеров и модулю сетевых операций. Например, вы не можете запустить таймер или подключить сокет в потоке, который не является потоком объекта объекта.
- Вы должны убедиться, что все объекты, созданные в потоке, удалены до удаления QThread. Это можно легко сделать, создав объекты в стеке в реализации вашего метода run().
Хотя QObject является реентерабельным, классы графического интерфейса, в частности QWidget и все его подклассы, не являются реентерабельными. Они могут использоваться только из главного потока. Как отмечалось ранее, QCoreApplication::exec() также должен вызываться из этого потока.
На практике невозможность использования классов графического интерфейса в других потоках, кроме главного, легко обходится путем размещения ресурсоёмких операций в отдельном рабочем потоке и отображения результатов на экране в главном потоке, когда рабочий поток завершит свою работу. Такой подход используется для реализации примера Мандельброта и примера блокирующего клиента Fortune.
В общем случае создание QObjects до QApplication не поддерживается и может приводить к странным сбоям при завершении работы в зависимости от платформы. Это означает, что статические экземпляры QObject также не поддерживаются. Правильно структурированное одно- или многопоточное приложение должно создавать QApplication как первый создаваемый и последний удаляемый QObject.
Очередь событий на поток
Каждый поток может иметь свою собственную очередь событий. Инициативный поток запускает свою очередь событий с помощью QCoreApplication::exec(), или для приложений с графическим интерфейсом, иногда с помощью QDialog::exec(). Другие потоки могут запустить очередь событий с помощью QThread::exec(). Подобно QCoreApplication, QThread предоставляет функцию exit(int) и слот quit().
Очередь событий в потоке позволяет потоку использовать определенные классы Qt, не относящиеся к графическому интерфейсу, которые требуют наличия очереди событий (например, QTimer, QTcpSocket и QProcess). Это также позволяет подключать сигналы из любых потоков к слотам определенного потока. Это более подробно объяснено в разделе Сигналы и слоты между потоками ниже.
Экземпляр QObject считается «обитающим» в том потоке, в котором он был создан. События для этого объекта обрабатываются очередью событий этого потока. Поток, в котором обитает QObject, доступен с помощью QObject::thread().
Функция QObject::moveToThread() изменяет привязанность потока для объекта и его потомков (объект нельзя переместить, если у него есть родитель).
Вызов delete объекта QObject из потока, отличного от потока, к которому этот объект «принадлежит» (или доступ к объекту другими способами) небезопасен, если вы не гарантируете, что объект в данный момент не обрабатывает события. Используйте QObject::deleteLater() вместо этого, и в очередь событий потока, к которому принадлежит объект, будет отправлено событие DeferredDelete, которое в конечном итоге будет обработано. По умолчанию, потоком, к которому «принадлежит» QObject, является поток, в котором QObject был создан, но не после вызова QObject::moveToThread().
Если очередь событий не запущена, события не будут доставлены объекту. Например, если вы создадите объект QTimer в потоке, но никогда не вызовете exec(), QTimer никогда не излучит свой сигнал timeout(). Вызов deleteLater() также не сработает. (Эти ограничения также применяются к главному потоку.)
Вы можете вручную отправлять события любому объекту в любом потоке в любое время, используя потокобезопасную функцию QCoreApplication::postEvent(). События будут автоматически обрабатываться очередью событий потока, в котором был создан объект.
Фильтры событий поддерживаются во всех потоках с ограничением, что объект отслеживания должен жить в том же потоке, что и отслеживаемый объект. Аналогично, QCoreApplication::sendEvent() (в отличие от postEvent()) может использоваться только для отправки событий объектам, живущим в том потоке, из которого вызывается функция.
Доступ к подклассам QObject из других потоков
QObject и все его подклассы не являются потокобезопасными. Это включает всю систему доставки событий. Важно помнить, что очередь событий может отправлять события вашему подклассу QObject во время доступа к объекту из другого потока.
Если вы вызываете функцию подкласса QObject, который не живёт в текущем потоке, и объект может получать события, вы должны защитить весь доступ к внутренним данным подкласса QObject с помощью мьютекса; в противном случае могут возникнуть сбои или другое нежелательное поведение.
Как и другие объекты, объекты QThread живут в том потоке, где был создан объект, — а не в потоке, созданном при вызове QThread::run(). Обычно небезопасно предоставлять слоты в подклассе QThread, если вы не защищаете переменные-члены с помощью мьютекса.
С другой стороны, вы можете безопасно излучать сигналы из реализации QThread::run(), поскольку излучение сигналов является потокобезопасным.
Сигналы и слоты между потоками
Qt поддерживает следующие типы подключения «сигнал-слот»:
- Автоматическое подключение (по умолчанию) Если сигнал излучается в потоке, к которому привязан принимающий объект, поведение такое же, как при Прямом подключении. В противном случае поведение такое же, как при Очередном подключении.
- Прямое подключение Слоты вызываются немедленно при излучении сигнала. Слоты выполняются в потоке отправителя, что необязательно является потоком получателя.
- Очередное подключение Слоты вызываются при возврате управления в очередь событий потока получателя. Слоты выполняются в потоке получателя.
-
Блокирующее очередное подключение Слоты вызываются так же, как и при Очередном подключении, за исключением того, что текущий поток блокируется до возврата слота.
Примечание: Использование этого типа для подключения объектов в одном потоке приведет к тупику.
-
Уникальное подключение Поведение такое же, как при Автоматическом подключении, но подключение создается только в том случае, если оно не дублирует существующее подключение. То есть, если тот же сигнал уже подключен к тому же слоту для той же пары объектов, подключение не создается, и connect() возвращает
false.
Тип подключения можно указать, передав дополнительный аргумент в connect(). Обратите внимание, что использование прямых подключений, когда отправитель и получатель находятся в разных потоках, небезопасно, если в потоке получателя запущена очередь событий, по той же причине, что и вызов любой функции для объекта, существующего в другом потоке, небезопасен.
QObject::connect() сам по себе является потокобезопасным.
Пример Мандельброта использует очередное подключение для взаимодействия между рабочим потоком и главным потоком. Чтобы избежать зависания очереди событий главного потока (и, как следствие, пользовательского интерфейса приложения), все вычисления фрактала Мандельброта выполняются в отдельном рабочем потоке. Поток излучает сигнал, когда он завершает отрисовку фрактала.
Аналогично, Пример блокирующего клиента Fortune использует отдельный поток для асинхронного взаимодействия с TCP-сервером.
© The Qt Company Ltd
Licensed under the GNU Free Documentation License, Version 1.3.
https://doc.qt.io/qt-5.15/threads-qobject.html