Home · All Classes · Main Classes · Grouped Classes · Modules · Functions

Поддержка Потоков в Qt

Qt предоставляет поддержку потоков в виде платформенно-независимых потоковых классов, потокобезопасного способа посылки сообщений и возможности установки соединений сигнал-слот через границы потоков. Это облегчает создание портируемых многопроцессорных приложений и использование преимуществ многопроцессорных машин. Мультипоточное программирование также полезная парадигма для выполнения занимающих продолжительное время действий без замораживания пользовательского интерфейса.

Более ранние версии Qt предлагали возможность постройки библиотеки без поддержки потоков. Начиная с Qt 4.0 потоки всегда доступны.

Данный документ предназначен для аудитории имеющей знания и опыт работы с многопоточными приложениями. Если Вы плохо знакомы с потоками, см. наш Список Рекомендованной Литературы.

Темы:

Потоковые Классы

Qt включает следующие потоковые классы:

Создание Потока

Для создания потока, определите подкласс QThread и заново реализуйте его функцию run(). Например:

    class MyThread : public QThread
    {
        Q_OBJECT

    protected:
        void run();
    };

    void MyThread::run()
    {
        ...
    }

Затем, создайте экземпляр объекта вашего потокового класса и вызовите QThread::start(). Код, который содержится в Вашей реализации функции run() будет выполнен в отдельном потоке. Создание потока подробно объясняется в документации QThread.

Обратите внимание, что QCoreApplication::exec() всегда должен вызываться из главного потока (потока, в котором выполняется main()), а не из QThread. В приложениях с GUI главный поток также назвается потоком GUI thread, потому, что только ему разрешается выполнять какие-либо действия, связанные с GUI.

Кроме того, до создания объектов QThread, Вы должны создать объект QApplication (или QCoreApplication).

Синхронизация Потоков

Классы QMutex, QReadWriteLock, QSemaphore и QWaitCondition предоставляют средства синхронизации потоков. В то время, как основная идея потоков состоит в том, чтобы сделать потоки настролько параллельными, насколько это возможно, бывают моменты, когда поток должет остановить выполнение текущих операций и подождать другие потоки.Например, если два потока одновременно пытаются получить доступ к одной глобальной переменной, то результат, обычно, не определен.

QMutex предоставляет взаимноисключающую блокировку или мьютекс. В одно и то-же время не больше одного потока может блокировать мьютекс. Если поток пытается заблокировать мьютекс в то время, как он уже заблокирован, то поток переходит в режим ожидания пока заблокировавший мютекс поток не освободит его (мьютекс). Мьютексы часто используются для защиты доступа к разделенным данным (т.е. данным, к которым можно обратиться от нескольких потоков одновременно). В разделе Однопоточность и Потоковая Безопасность ниже, мы используем мьютексы для создания потокобезопасного класса.

QReadWriteLock подобен QMutex, за исключением того, что делает различие между доступом к данным для "чтения" и "записи" и позволяет нескольким читателям одновременно обращаться к данным. По возможности используя QReadWriteLock вместо QMutex можно сделать многопоточную программу более параллельными.

QSemaphore - это обобщение для QMutex которое защищает некоторое количество идентичных ресурсов. В отличие от мьютекса, защищающего один ресурс. Пример в описании Семафоров показывает типичное использование семафоров: синхронизированние доступа производителя и потребителя к кольцевому буферу.

QWaitCondition позволяет потоку пробуждать другие потоки при выполнении некоторого условия. Один или несколько потоков могут быть заблокированы в ожидании выполнения QWaitCondition установленного в состояние wakeOne() или wakeAll(). При использовании wakeOne() потоки пробуждаются при выполнении одного из нужных условий, а wakeAll() требует выполнения всех условий. Пример Условие Ожидания показывает как решить проблему производитель-потребитель используя QWaitCondition вместо QSemaphore.

Монопоточность и Потоковая Безопасность

Везде в документации 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++ ++ и -- не всегда атомарны. В действительности они обычно расширяются до трех машинных инструкций:

  1. Загрузка значения переменной в регистр.
  2. Увеличение или уменьшение значения регистра.
  3. Сохранение значения регистра обратно в основную память.

Потоки 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().

Потоки и Объекты QObject

QThread наследует QObject. Он испускает сигналы, сообщающие о том, что поток начал или закончил работу, а также предоставляет несколько слотов.

Очень интересно использование QObject в многопоточном приложении, он испускает сигналы, приходящие в слоты, находящиеся в других потоках, и посылает сообщения объектам "живущим" в других потоках. Это возможно потому, что каждый поток имеет собственный цикл обработки сообщений.

Монопоточность QObject

QObject монопоточен. Большинство из его не-GUI подклассов, таких как QTimer, QTcpSocket, QUdpSocket, QHttp, QFtp и QProcess, также межпоточны. Их возможно использовать из нескольких потоков одновременно. Вы должны знать о двух ограничениях:

Несмотря на то, что QObject межпоточен, классы GUI, особенно QWidget и все его подклассы, не межпоточны. Они могут использоваться только в главном потоке. Как отмечено выше, QCoreApplication::exec() также может вызываться из главного потока.

На практике невозможно использовать классы GUI в других потоках, кроме главного, но можен легко поместить выполнение продолжительных действий в отдельный поток и отображать на экране результаты выполнения средствами главного потока по окончании обработки. Такой подход используется в примерах Mandelbrot и Blocking Fortune Client.

Цикл Обработки Сообщений Потока

Каждый поток может иметь собственный цикл обработки сообщений. Главный поток начинает цикл обработки сообщений используя QCoreApplication::exec(); другие потоки могут начать свои циклы обработки сообщений используя QThread::exec(). Подобно QCoreApplication, QThread предоставляет функцию exit(int) и слот quit().

Цикл обработки сообщений сделан возможным для потока, чтобы можно было использовать некоторые не-GUI классы Qt которые требуют наличия цикла обработки сообщений (такие как QTimer, QTcpSocket и QProcess). Это также доет возможность соединить сигналы из любых потоков со слотами в определенном потоке. В разделе Соединение Сигналов и Слотов Между Потоками это описано подробнее.

Потоки, объекты и цикл обработки сообщений

Экземпляр QObject считается живущим в потоке, в котором был создан. Сообщения этому объекту пересылаются циклом обработки сообщений потока. Поток в котором живет QObject может быть получен с помощью QObject::thread().

Вызов 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 поддерживает два типа соединений сигнал-слот:

По умолчанию QObject::connect() устанавливает прямую связь, если отправитель и получатель принадлежат одному потоку, и связь с постановкой в очередь, если отправитель и получатель принадлежат различным потокам. Это можно изменить, передав дополнительный аргумент в connect(). Помните, что использование прямых связей, когда отправитель и получатель проживают в разных потоках опасно в случае, если цикл обработки сообщений выполняется в потоке, где живет приемник, по той-же самой причине, по которой небезопасен вызов функций объекта, принадлежащего другому потоку.

QObject::connect() само по себе потокобезопасно.

В примере Mandelbrot используется соединение с постановкой в очередь для установки связи между рабочим и основным потоками. Для того, чтобы избежать заморозки цикла обработки сообщений основного потока (и, как следствие, заморозки пользовательского интерфейса приложения), все рекурсивные вычисления фрактала в Mandelbrot выполняются в отдельно потоке. Это поток испускает сигнал после вычислений, который рисует фрактал.

Точно также, в примере Blocking Fortune Client используется отдельный поток для асинхронной связи с TCP-сервером.

Потоки и Неявное Совместное Использование Данных

Qt использует оптимизацию, называемую неявным совместным использованием данных для многих своих классов, особенно QImage и QString. Многие считеют, что неявное совместное использование данных и много поточность - несовместимые концепции из-за способа, которым обычно выполняется подсчет ссылок. Одно из решений состоит в том, чтобы защитить внутренний счетчик ссылок с помощью мьютекса, но это очень медленно. Более ранние версии Qt не предоставляли удовленворительного решения этой проблемы.

Начиная с Qt 4, классы использующие неявное совместное использование данных могут быть безопасно скопированы между потоками подобно любым другим классам. Они полностью межпоточны. Неявное совместное использование данных действительно неявно. Это реализовано с использованием атомарных действий подсчета ссылок на ассемблере для различных платформ, поддерживаемых Qt. Атомарный подсчет ссылок очень быстр, намного быстрее, чем использование мьютекса.

Но напоминаем, если вы получаете доступ к одному и тому-же объекту из нескольких потоков (в отличие от копий одного и того-же объекта), Вы все еще должны использовать мьютекс для упорядочивания доступа к объекту так-же как и при работе с любым межпоточным классом.

Резюмируя скажем, что неявное совместное использование данных в Qt 4 действительно неявное разделение. Даже в многопоточных приложениях Вы можете благополучно применять классы, использующие неявное разделение данных, так, будто это простые межпоточные классы.

Потоки и Модуль SQL

Соединение может использоваться только внутри создавшего его потока. Перемещение соединений между потоками и создание запросов в другой поток не поддерживается.

Кроме того, библиотеки третьих лиц, использующие драйвера QSqlDriver могут наложить дополнительные ограничения на использование Модуля SQL в многопоточной программе. За дополнительной информацией обращайтесь к создателю клиента базы данных.

Рекомендуемая Литература


Copyright © 2005 Trolltech Trademarks
Qt 4.1.0
Hosted by uCoz