Поддержка потоков в 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 реентерабелен. Большинство из его неграфических (non-GUI) подклассов, таких как QTimer, QTcpSocket, QUdpSocket, QHttp, QFtp и QProcess, также реентерабельны, что делает возможным их использование из нескольких потоков одновременно. Заметим, что эти классы спроектированы для создания и использования в одном потоке; создание объекта в одном потоке и вызов его функций из другого может и не сработать. Вы должны знать о трёх ограничениях:
Несмотря на то, что QObject реентерабелен, классы ГПИ, особенно QWidget и все его подклассы, таковыми не являются. Они могут использоваться только из главного потока. Как было сказано ранее, QCoreApplication::exec() также должен вызываться из главного потока. На практике невозможно использовать классы ГПИ в других потоках, кроме главного, но выполнение продолжительных действий можно легко поместить в отдельный поток и отображать на экране результаты выполнения средствами главного потока по окончании обработки во вспомогательном потоке. Такой подход используется в примерах Мандельброт и Блокирующий клиент Fortune. Цикл обработки событий потокаКаждый поток может иметь собственный цикл обработки событий. Главный поток начинает цикл обработки событий, используя QCoreApplication::exec(); другие потоки могут начать свои циклы обработки событий, используя QThread::exec(). Подобно QCoreApplication, QThread предоставляет функцию exit(int) и слот quit(). Цикл обработки событий потока делает возможным использование потоком некоторых неграфических классов 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() сама по себе потокобезопасна. В примере Мандельброт используется соединение с постановкой в очередь для установки связи между рабочим и основным потоками. Для того, чтобы избежать "заморозки" цикла обработки событий основного потока (и, как следствие, "заморозки" пользовательского интерфейса приложения), все рекурсивные вычисления фрактала Мандельброта выполняются в отдельном потоке. Этот поток испускает сигнал после окончания вычислений, который рисует фрактал. Точно также, в примере Блокирующий клиент Fortune используется отдельный поток для асинхронной связи с TCP-сервером. Потоки и неявное совместное использование данныхQt использует оптимизацию, называемую неявным совместным использованием данных для многих своих классов, особенно QImage и QString. Многие считают, что неявное совместное использование данных и многопоточность - несовместимые концепции из-за способа, которым обычно выполняется подсчет ссылок. Одно из решений состоит в том, чтобы защитить внутренний счетчик ссылок с помощью мьютекса, но это очень медленно. Более ранние версии Qt не предоставляли удовлетворительного решения этой проблемы. Начиная с Qt 4, классы, использующие неявное совместное использование данных, могут быть безопасно скопированы между потоками подобно любым другим классам. Они полностью реентерабельны. Неявное совместное использование данных действительно является таковым. Это реализовано с использованием атомарных действий подсчета ссылок на ассемблере для различных платформ, поддерживаемых Qt. Атомарный подсчет ссылок очень быстр, намного быстрее, чем использование мьютекса (смотрите также Выполнение атомарных операций). Но напоминаем, если вы получаете доступ к одному и тому-же объекту из нескольких потоков (в отличие от копий одного и того-же объекта), вы все еще должны использовать мьютекс для упорядочивания доступа к объекту так-же как и при работе с любым реентерабельным классом. Резюмируя скажем, что неявно совместно используемые классы в Qt 4 действительно является неявно совместно используемыми. Даже в многопоточных приложениях вы можете благополучно применять классы, использующие неявное разделение данных, так, будто это простые реентерабельные классы. Потоки и модуль SQLСоединение может использоваться только внутри создавшего его потока. Перемещение соединений между потоками и создание запросов в другой поток не поддерживается. Кроме того, используемые драйверами QSqlDriver сторонние библиотеки могут наложить дополнительные ограничения на использование модуля SQL в многопоточной программе. За дополнительной информацией обращайтесь к руководству по клиенту вашей базы данных. Рекомендуемая литература
|
Попытка перевода Qt документации. Если есть желание присоединиться, или если есть замечания или пожелания, то заходите на форум: Перевод Qt документации на русский язык... Люди внесшие вклад в перевод: Команда переводчиков |