Потоки и 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/archives/qt-5.6/threads-qobject.html