Поддержка потоков в Qt
|
Тип итератора | Классы примера | Статус поддержки |
---|---|---|
Итератор ввода (Input Iterator) | Не поддерживается | |
Итератор вывода (Output Iterator) | Не поддерживается | |
Итератор пересылки (Forward Iterator) | std::slist | Поддерживается |
Двунаправленный итератор (Bidirectional Iterator) | QLinkedList, std::list | Поддерживается |
Итератор случайного доступа | QList, QVector, std::vector | Поддерживается и рекомендуется |
В тех случаях, когда Qt Concurrent осуществляет перебор большого количества небольших элементов итераторы случайного доступа могут быть быстрее, так как они позволяют перепрыгивать (skipping) в любую точку в контейнере. Кроме того, использование итераторов с произвольным доступом разрешает Qt Concurrent предоставить информацию о прогрессе выполнения посредством QFuture::progressValue() и QFutureWatcher:: progressValueChanged().
Функции, не изменяющие элементы на месте, такие как mapped() и filtered(), создают при вызове копию контейнера. Если вы используете контейнеры STL, эти операции копирования могут занять некоторое время, вместо этого мы рекомендуем в этом случае определить начальный и конечный итераторы для контейнера.
Везде в документации 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().
QThread унаследован от QObject. Он испускает сигналы, сообщающие о том, что поток начал или закончил работу, а также предоставляет несколько слотов.
Более интересным является то, что объекты QObject могут использоваться в многопоточном приложении, испускать сигналы, приходящие в слоты, находящиеся в других потоках, и посылать события объектам, "живущим" в других потоках. Это возможно, потому что каждый поток имеет собственный цикл обработки событий.
QObject реентерабелен. Большинство из его неграфических (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 с помощью мьютекса; в противном случае вы можете получить крах программы или другое неожиданное поведение.
Подобно другим объектам, QThread "живет" в потоке, в котором он был создан - не в потоке, который создан при вызове QThread::run(). Вообще, опасно иметь слоты в вашем подклассе QThread, если вы не защищаете переменные-члены с помощью мьютекса.
С другой стороны, вы можете спокойно испускать сигналы вашей реализацией QThread::run(), потому что испускание сигналов потокобезопасно.
Qt поддерживает два типа соединений сигнал-слот.
Это можно изменить, передав дополнительный аргумент в connect(). Помните, что использование прямых связей, когда отправитель и получатель "живут" в разных потоках, опасно в случае, если цикл обработки событий выполняется в потоке, где "живет" приемник, по той же самой причине, по которой небезопасен вызов функций объекта, принадлежащего другому потоку.
QObject::connect() сама по себе потокобезопасна.
В примере Мандельброт используется соединение с постановкой в очередь для установки связи между рабочим и основным потоками. Для того, чтобы избежать "заморозки" цикла обработки событий основного потока (и, как следствие, "заморозки" пользовательского интерфейса приложения), все рекурсивные вычисления фрактала Мандельброта выполняются в отдельном потоке. Этот поток испускает сигнал после окончания вычислений, который рисует фрактал.
Точно также, в примере Блокирующий клиент Fortune используется отдельный поток для асинхронной связи с TCP-сервером.
Qt использует оптимизацию, называемую неявным совместным использованием данных для многих своих классов, особенно QImage и QString. Начиная с Qt 4, классы, использующие неявное совместное использование данных, могут быть безопасно скопированы между потоками подобно любым другим классам. Они полностью реентерабельны (reentrant). Неявное совместное использование данных действительно является таковым.
Многие считают, что неявное совместное использование данных и многопоточность - несовместимые концепции из-за способа, которым обычно выполняется подсчет ссылок. Тем не менее, Qt использует атомарный (atomic) подсчет ссылок, чтобы гарантировать целостность совместно используемых данных, избегая потенциальной порчи счетчика ссылок.
Обратите внимание на то, что атомарный подсчет ссылок не гарантирует потокобезопасность. Надо использовать подходящую блокировку при совместном использовании экземпляра класса, неявно разделяемого между потоками. То же требование распространяется на все реентерабельные классы, совместно используемые или нет. Тем не менее, атомарный подсчет ссылок дает гарантию, что поток, работающий в своем собственном, локальном экземпляре неявно разделяемого класса, является безопасным. Мы рекомендуем использовать сигналы и слоты для передачи данных между потоками, поскольку это может быть сделано без необходимости в каких-либо явных (explicit) блокировках.
Резюмируя скажем, что неявно совместно используемые классы в Qt 4 действительно является неявно совместно используемыми. Даже в многопоточных приложениях вы можете безопасно использовать их как если бы они были простыми (plain), не используемыми совместно, реентерабельными классами, основанными на значениях.
Соединение может использоваться только внутри создавшего его потока. Перемещение соединений между потоками и создание запросов в другой поток не поддерживается.
Кроме того, используемые драйверами QSqlDriver сторонние библиотеки могут наложить дополнительные ограничения на использование модуля SQL в многопоточной программе. За дополнительной информацией обращайтесь к руководству по клиенту вашей базы данных.
QPainter может быть использован для рисования на устройствах рисования QImage, QPrinter и QPicture. Рисование на QPixmap и QWidget не поддерживается. В Mac OS X автоматический диалог выполнения не будет отображаться на экране, если вы печатаете за пределами GUI-потока.
Любое количество потоков могут рисовать в любое заданное время, однако в каждый момент времени только один поток может рисовать на заданном устройстве рисования. Другими словами, два потока могут рисовать в один и тот же момент времени, если каждый из них рисует на разных объектах QImage, но два потока не могут одновременно рисовать на одном и том же объекте QImage.
Обратите внимание на то, что в системах X11 без поддержки FontConfig Qt не может отрисовывать текст за пределами ГПИ-потока. Вы можете использовать функцию QFontDatabase::supportsThreadedFontRendering() для определения, может ли быть отрисован шрифт за пределами ГПИ-потока или нет.
Классы QTextDocument, QTextCursor и все родственные классы являются реентерабельными.
Обратите внимание на то, что экземпляр класса QTextDocument, созданный в ГПИ-потоке, может содержать ресурсы изображений QPixmap. Используйте QTextDocument::clone() для создания копии документа и передавайте копию другому потоку для дальнейшей обработки (например, печати).
Классы QSvgGenerator и QSvgRenderer в модуле QtSvg являются реентерабельными.
Copyright © 2009 Nokia Corporation and/or its subsidiary(-ies) | Торговые марки | Qt 4.5.3 |
Попытка перевода Qt документации. Если есть желание присоединиться, или если есть замечания или пожелания, то заходите на форум: Перевод Qt документации на русский язык... Люди внесшие вклад в перевод: Команда переводчиков |