Поддержка потоков в Qt
Qt предоставляет поддержку потоков в виде платформо-независимых потоковых классов, потокобезопасного способа отправки сообщений и возможности установки соединений сигнал-слот между потоками. Это облегчает создание переносимых многопоточных приложений и использование преимуществ многопроцессорных машин. Многопоточное программирование - также полезная парадигма для выполнения занимающих продолжительное время действий без замораживания пользовательского интерфейса. Более ранние версии Qt предлагали возможность собрать библиотеку без поддержки потоков. Начиная с Qt 4.0 потоки всегда доступны. Данный документ предназначен для аудитории, имеющей знания и опыт работы с многопоточными приложениями. Если вы плохо знакомы с потоками, смотрите наш список Рекомендованной литературы. Темы:
Потоковые классыQt включает следующие потоковые классы:
Примечание: Классы работы с потоками Qt реализуются с помощью родных средств API; например, Win32 и pthreads. Потому они могут взаимодействовать с родными потоками этого API. Создание потокаДля создания потока, определите подкласс QThread и заново реализуйте его функцию run(). Например: class MyThread : public QThread { Q_OBJECT protected: void run(); }; void MyThread::run() { ... } Затем создайте экземпляр объекта вашего потокового класса и вызовите QThread::start(). Код, который содержится в вашей реализации функции run(), будет выполнен в отдельном потоке. Создание потока подробно объясняется в документации QThread. Обратите внимание на то, что QCoreApplication::exec() всегда должен вызываться из главного потока (потока, в котором выполняется main()), а не из QThread. В приложениях с графическим интерфейсом пользователя главный поток также называется потоком GUI, потому что только ему разрешается выполнять какие-либо действия, связанные с графическим интерфейсом. Кроме того, вы должны создать объект QApplication (или QCoreApplication) до создания объектов QThread. Синхронизация потоковКлассы QMutex, QReadWriteLock, QSemaphore и QWaitCondition предоставляют средства синхронизации потоков. Хотя, основная идея потоков состоит в том, чтобы сделать потоки настолько параллельными, насколько это возможно, бывают моменты, когда поток должен остановить выполнение текущих операций и подождать другие потоки. Например, если два потока одновременно пытаются получить доступ к одной глобальной переменной, то результат, обычно, не определен. QMutex предоставляет взаимоисключающую блокировку или мьютекс. В одно и то же время не больше одного потока может блокировать мьютекс. Если поток пытается заблокировать мьютекс в то время, как он уже заблокирован, то поток переходит в режим ожидания пока заблокировавший мютекс поток не освободит его (мьютекс). Мьютексы часто используются для защиты доступа к разделяемым данным (т.е. данным, к которым можно обратиться из нескольких потоков одновременно). Ниже, в разделе Реентерабельность и потоковая безопасность, мы используем мьютексы для создания потокобезопасного класса. QReadWriteLock подобен QMutex за исключением того, что делает различие между доступом к данным для "чтения" и "записи" и позволяет нескольким читателям одновременно обращаться к данным. Используя, когда это возможно, QReadWriteLock вместо QMutex можно сделать многопоточную программу более параллельной. QSemaphore - это обобщение для QMutex, которое защищает некоторое количество идентичных ресурсов. В отличие от мьютекса, защищающего один ресурс. В примере Семафоры показано типичное использование семафоров: синхронизация доступа производителя и потребителя к кольцевому буферу. QWaitCondition позволяет потоку пробуждать другие потоки при выполнении некоторого условия. Один или несколько потоков могут быть заблокированы в ожидании выполнения QWaitCondition, установленного в состояние wakeOne() или wakeAll(). Используйте wakeOne() для пробуждения одного случайно выбранного потока или wakeAll() для всех. Пример Условия ожидания показывает, как решить проблему производитель-потребитель используя QWaitCondition вместо QSemaphore. Обратите внимание на то, что классы синхронизации Qt зависят от использования правильно выровненных (properly aligned) указателей. Например, вы не можете использовать упакованные классы вместе с MSVC. Реентерабельность и потокобезопасностьВезде в документации Qt, термины реентерабельность и потокобезопасность используются для определения того, как функции могут использоваться в многопоточных приложениях:
Более широко, класс считается реентерабельным если любая из его функций может быть вызвана одновременно из разных потоков для различных экземпляров класса. Аналогично, класс может быть назван потокобезопасным, если функции могут быть вызваны из различных потоков у одного экземпляра. Классы в этой документации помечены как потокобезопасные, только если они предназначены для использования в многопоточных приложениях. Помните, что терминология в этой области ещё не стандартизирована. POSIX использует несколько отличные определения реентерабельности и потокобезопасности в своих API C. Когда имеешь дело с объектно-ориентированной библиотекой классов C++, такой как Qt, определения должны быть адаптированы. Большинство классов-наследников классов C++ являются реентерабельными, поскольку обычно они работают с данными членов класса. Любой поток может вызвать функцию-член экземпляра класса, пока другой поток не вызывает функцию-член того же самого экземпляра класса. Например, нижеуказанный класс Counter является реентерабельным: class Counter { public: Counter() { n = 0; } void increment() { ++n; } void decrement() { --n; } int value() const { return n; } private: int n; }; Данный класс не является потокобезопасным, поскольку если несколько потоков попытаются изменить член данных n, результат будет неопределен. Это так, потому что C++ операторы ++ и -- не всегда атомарны. В действительности, они обычно расширяются до трех машинных инструкций:
Потоки A и B одновременно могут загрузить старое значение переменной, увеличить ее значение в регистре и сохранить значение переменной в памяти, но переменная будет увеличена только однажды! Поток A должен выполнить шаги 1, 2, 3 без прерывания (автоматического) прежде, чем поток B сможет выполнить те же шаги; или наоборот. Самый легкий способ создания потокобезопасного класса состоит в том, чтобы защитить весь доступ к членам данных с помощью QMutex: class Counter { public: Counter() { n = 0; } void increment() { QMutexLocker locker(&mutex); ++n; } void decrement() { QMutexLocker locker(&mutex); --n; } int value() const { QMutexLocker locker(&mutex); return n; } private: mutable QMutex mutex; int n; }; Класс QMutexLocker автоматически запирает мьютекс в своем конструкторе и отпирает его в деструкторе, вызываемом при завершении функции. Запирание мьютекса гарантирует, что обращения из разных потоков будут упорядочены. Член данных mutex объявлен как mutable, потому что позволяет запереть и отпереть мьютекс в функции value(), которая является константной. Большинство классов Qt явлюятся реентерабельными и не потокобезопасными для того, чтобы избежать многократного запирания и отпирания QMutex. Например, QString является реентерабельным, это означает, что вы можете использовать его в различных потоках, но вы не можете получить доступ к одному и тому же объекту QStringодновременно из различных потоков (если вы самостоятельно не защищаете его с помощью мьютекса). Несколько классов и функций являются потокобезопасными; они, главным образом, связаны с потоковыми классами, такими как QMutex, или фундаментальными функциями, такими как QCoreApplication::postEvent(). Потоки и объекты QObjectQThread унаследован от QObject. Он испускает сигналы, сообщающие о том, что поток начал или закончил работу, а также предоставляет несколько слотов. Более интересным является то, что объекты QObject могут использоваться в многопоточном приложении, испускать сигналы, приходящие в слоты, находящиеся в других потоках, и посылать сообщения объектам, "живущим" в других потоках. Это возможно, потому что каждый поток имеет собственный цикл обработки сообщений. Реентерабельность QObjectQObject реентерабелен. Большинство из его не-GUI подклассов, таких как QTimer, QTcpSocket, QUdpSocket, QHttp, QFtp и QProcess, также реентерабельны, что делает возможным их использование из нескольких потоков одновременно. Заметим, что эти классы спроектированы для создания и использования в одном потоке; создание объекта в одном потоке и вызов его функций из другого может и не сработать. Вы должны знать о трёх ограничениях:
Несмотря на то, что QObject реентерабелен, классы графического интерфейса пользователя, особенно QWidget и все его подклассы, таковыми не являются. Они могут использоваться только из главного потока. Как было сказано ранее, QCoreApplication::exec() также должен вызываться из главного потока. На практике невозможно использовать классы графического интерфейса пользователя в других потоках, кроме главного, но выполнение продолжительных действий можно легко поместить в отдельный поток и отображать на экране результаты выполнения средствами главного потока по окончании обработки во вспомогательном потоке. Такой подход используется в примерах Mandelbrot и Blocking Fortune Client. Цикл обработки сообщений потокаКаждый поток может иметь собственный цикл обработки сообщений. Главный поток начинает цикл обработки сообщений, используя QCoreApplication::exec(); другие потоки могут начать свои циклы обработки сообщений, используя QThread::exec(). Подобно QCoreApplication, QThread предоставляет функцию exit(int) и слот quit(). Цикл обработки сообщений сделан возможным для потока, чтобы можно было использовать некоторые не-GUI классы Qt, которые требуют наличия цикла обработки сообщений (такие как QTimer, QTcpSocket и QProcess). Это также даёт возможность соединить сигналы из любых потоков со слотами в определённом потоке. В разделе Соединение сигналов и слотов между потоками это описано подробнее. Экземпляр QObject считается живущим в потоке, в котором он был создан. Сообщения этому объекту пересылаются циклом обработки сообщений потока. Поток, в котором живет QObject, может быть получен с помощью QObject::thread(). Помните, что для объектов QObject, которые созданы до QApplication, QObject::thread() возвратит ноль. Это означает, что только главный поток может обрабатывать события для этих объектов; для объектов без потока обработка событий не происходит. Используйте функцию QObject::moveToThread() для изменения принадлежности к потоку объекта и его потомков (объект не может быть перемещён, если имеет предков). Вызов delete для объекта QObject (и вообще обращение к объекту) из потока, отличного от того, в котором он был создан может быть опасен, если нельзя быть уверенным, что объект не обрабатывает другие сообщения в этот момент. Вместо этого используйте QObject::deleteLater(); объекту будет послано сообщение DeferredDelete, которое, в конце концов, будет обработано циклом обработки сообщений данного объекта. Если никакой цикл обработки сообщений не запущен, то сообщения не будут доставлены объекту. Например, если вы создаёте объект QTimer в потоке, который никогда не вызывает exec(), то QTimer никогда не испустит сигнал timeout(). Вызов deleteLater() также не сработает. (Это также относится к главному потоку.) Вы можете вручную послать сообщение любому объекту в любом потоке используя потокобезопасную функцию QCoreApplication::postEvent(). Сообщения будут автоматически посланы циклу обработки сообщений потока, в котором объект был создан. Фильтры сообщений поддерживаются во всех потоках, с условием, что контролируемый объект должен располагаться в том же потоке, что и контролирующий объект. Также QCoreApplication::sendEvent() (в отличие от postEvent()) может использоваться только для посылки сообщений живущим в том же потоке, что и посылающая сообщения функции. Организация доступа к подклассам QObject из других потоковQObject и все его подклассы не потокобезопасны. Это влияет на всю систему доставки сообщений. Важно помнить, что цикл обработки сообщений может доставлять сообщения вашему подклассу QObject в то время, как вы обращаетесь к объекту из другого потока. Если вы вызываете функцию подкласса QObject, не живущего в текущем потоке, и объект может получать сообщения, то вы должны защитить все обращения к данным вашего подкласса QObject с помощью мьютекса; в противном случае вы можете получить крах программы или другое неожиданное поведение. Подобно другим объектам, QThread живет в потоке, в котором он был создан -- не в потоке, который создан при вызове QThread::run(). Вообще, опасно иметь слоты в вашем подклассе QThread, если вы не защищаете переменные-члены с помощью мьютекса. С другой стороны, вы можете спокойно испускать сигналы вашей реализацией QThread::run(), потому что испускание сигналов потокобезопасно. Соединение сигналов и слотов между потокамиQt поддерживает два типа соединений сигнал-слот.
Это можно изменить, передав дополнительный аргумент в connect(). Помните, что использование прямых связей, когда отправитель и получатель проживают в разных потоках опасно в случае, если цикл обработки сообщений выполняется в потоке, где живет приемник, по той же самой причине, по которой небезопасен вызов функций объекта, принадлежащего другому потоку. QObject::connect() сама по себе потокобезопасна. В примере Mandelbrot используется соединение с постановкой в очередь для установки связи между рабочим и основным потоками. Для того, чтобы избежать "заморозки" цикла обработки сообщений основного потока (и, как следствие, "заморозки" пользовательского интерфейса приложения), все рекурсивные вычисления фрактала в Mandelbrot выполняются в отдельном потоке. Этот поток испускает сигнал после окончания вычислений, который рисует фрактал. Точно также, в примере Блокирующий клиент Fortune используется отдельный поток для асинхронной связи с TCP-сервером. Потоки и неявное совместное использование данныхQt использует оптимизацию, называемую неявным совместным использованием данных для многих своих классов, особенно QImage и QString. Многие считают, что неявное совместное использование данных и многопоточность - несовместимые концепции из-за способа, которым обычно выполняется подсчет ссылок. Одно из решений состоит в том, чтобы защитить внутренний счетчик ссылок с помощью мьютекса, но это очень медленно. Более ранние версии Qt не предоставляли удовлетворительного решения этой проблемы. Начиная с Qt 4, классы, использующие неявное совместное использование данных, могут быть безопасно скопированы между потоками подобно любым другим классам. Они полностью реентерабельны. Неявное совместное использование данных действительно является таковым. Это реализовано с использованием атомарных действий подсчета ссылок на ассемблере для различных платформ, поддерживаемых Qt. Атомарный подсчет ссылок очень быстр, намного быстрее, чем использование мьютекса (смотрите также Выполнение атомарных операций). Но напоминаем, если вы получаете доступ к одному и тому-же объекту из нескольких потоков (в отличие от копий одного и того-же объекта), вы все еще должны использовать мьютекс для упорядочивания доступа к объекту так-же как и при работе с любым реентерабельным классом. Резюмируя скажем, что неявное совместное использование данных в Qt 4 действительно является неявным разделением. Даже в многопоточных приложениях вы можете благополучно применять классы, использующие неявное разделение данных, так, будто это простые реентерабельные классы. Потоки и модуль SQLСоединение может использоваться только внутри создавшего его потока. Перемещение соединений между потоками и создание запросов в другой поток не поддерживается. Кроме того, используемые драйверами QSqlDriver сторонние библиотеки могут наложить дополнительные ограничения на использование модуля SQL в многопоточной программе. За дополнительной информацией обращайтесь к руководству по клиенту вашей базы данных. Рекомендуемая литература
|
Попытка перевода Qt документации. Если есть желание присоединиться, или если есть замечания или пожелания, то заходите на форум: Перевод Qt документации на русский язык... Люди внесшие вклад в перевод: Команда переводчиков |