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