Потоки и QObjects
QThread наследуется от QObject. Он излучает сигналы, чтобы указать, что поток начал или завершил выполнение, а также предоставляет несколько слотов.
Более интересно то, что QObjects могут использоваться в нескольких потоках, излучать сигналы, вызывающие слоты в других потоках, и публиковать события в объекты, которые «живут» в других потоках. Это возможно, потому что каждый поток может иметь свою собственную очередь событий.
Взаимная рекурсия QObject
QObject является рекурсивным. Большинство его подклассов, не связанных с графическим интерфейсом, такие как QTimer, QTcpSocket, QUdpSocket и QProcess, также являются рекурсивными, что позволяет использовать эти классы из нескольких потоков одновременно. Обратите внимание, что эти классы предназначены для создания и использования в пределах одного потока; создание объекта в одном потоке и вызов его функций из другого потока не гарантируется. Следует учитывать три ограничения:
-
Дочерний объект QObject всегда должен создаваться в том потоке, где был создан родительский объект. Это подразумевает, в том числе, что вы никогда не должны передавать объект QThread (
this) в качестве родителя объекта, созданного в потоке (поскольку сам объект QThread был создан в другом потоке). - Объекты, управляемые событиями, могут использоваться только в одном потоке. В частности, это относится к механизму таймера и модулю сети. Например, вы не можете запустить таймер или подключить сокет в потоке, который не является потоком объекта.
- Вы должны убедиться, что все объекты, созданные в потоке, удалены перед удалением объекта QThread. Это можно легко сделать, создавая объекты на стеке в реализации метода run().
Хотя QObject является рекурсивным, классы графического интерфейса, в частности QWidget и все его подклассы, не являются рекурсивными. Они могут использоваться только из основного потока. Как отмечалось ранее, QCoreApplication::exec() также должен вызываться из этого потока.
На практике невозможность использования классов графического интерфейса в других потоках, кроме основного, легко обходится, помещая трудоемкие операции в отдельный рабочий поток, а результаты отображая на экране в основном потоке, когда рабочий поток завершается. Такой подход используется для реализации Примера Mandelbrot и Примера блокирующего клиента 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() сам по себе является потокобезопасным.
Пример Mandelbrot использует подключение в очереди для связи между рабочим потоком и главным потоком. Чтобы избежать зависания очереди событий основного потока (и, как следствие, пользовательского интерфейса приложения), все вычисления фрактала Mandelbrot выполняются в отдельном рабочем потоке. Поток излучает сигнал, когда он закончит отрисовку фрактала.
Аналогично, Пример блокирующего клиента Fortune использует отдельный поток для асинхронного взаимодействия с TCP-сервером.
© The Qt Company Ltd
Licensed under the GNU Free Documentation License, Version 1.3.
https://doc.qt.io/qt-5.13/threads-qobject.html