|
|||||||
Qt и многопоточность: Потоки без цикла обработки событий
Время создания: 23.03.2023 16:09
Автор: Андрей Боровский
Текстовые метки: c++, qt, поток, многопоточность, QThread
Раздел: Компьютер - Программирование - Язык C++ (Си++) - Библиотека Qt - Многопоточность
Запись: xintrea/mytetra_syncro/master/base/1679576964jkl1fudum4/text.html на raw.github.com
|
|||||||
|
|||||||
Возможность запуска собственного цикла обработки событий в потоках Qt, появившаяся в версии Qt 4, очень удобна, если использовать обычную для Qt модель приложения, которое управляется событиями и сигналами. Но иногда мы создаем потоки именно для того, чтобы избежать модели "сигнал–слот". Иногда нам просто нужно выполнить какую-то длительную операцию с возможностью ее досрочного завершения или временной приостановки. Использовать слоты для этой цели неудобно. Допустим, мы поместим нашу длительную операцию в один слот. Во время работы этого слота цикл обработки сообщений просто ничего не может сделать. Можно, конечно, периодически активировать цикл из нашего слота с помощью метода processMessages() или чего-то подобного, но если в процессе работы нашего "длительного" слота будет получен сигнал, требующий корректного завершения потока, этот сигнал не сможет сделать ничего, пока слот не завершит работу. Иначе говоря, в такой ситуации стандартный сигнал корректного завершения будет просто бесполезен. В этом случае гораздо эффективнее будет другая модель потока, которую мы сейчас и реализуем. Класс ExtThread (листинг 5.8) является частью программы exthreads, полный текст которой вы найдете в папке Ch5/extthreads на CD-ROM диске к книге «Qt4.7+. Практическое программирование на C++» Листинг 5.8. Объявление класса потока ExtThread #ifndef EXTTHREAD_H #define EXTTHREAD_H #include <QThread> #include <QAtomicInt> enum ExtThreadStates { Ready, Working, Sleeping, Finishing, Finished }; class ExtThread : public QThread { Q_OBJECT public: explicit ExtThread(QObject *parent = 0); bool pause(); bool resume(); bool wait(unsigned long time = ULONG_MAX); ExtThreadStates getCurrentState(); void start();
signals: void paused(); void resumed();
public slots: void quit();
protected: bool CancellationPoint(); bool pauseFor(unsigned long milliseconds = ULONG_MAX); virtual void beforeQuit(); void done();
private: QAtomicInt currentState; }; #endif // EXTTHREAD_H Класс ExtThread перекрывает некоторые методы класса QThread и вводит некоторые новые методы. Мы разделим эти методы на две группы: методы, предназначенные для управления потоком извне (обычно их вызывает тот поток, который создает экземпляр ExtThread ), и методы, которые используются внутри метода run(), т. е. в контексте самого потока. К первой группе относятся конструктор ExtThread(), методы start(), pause(), resume(), wait(), getCurrentState() и слот quit(). Ко второй группе относятся методы CancellationPoint(), pauseFor(), beforeQuit() и done(). Вы также можете пользоваться методами, унаследованными от QThread, за исключением тех методов, которые работают с очередью обработки событий. ПРИМЕЧАНИЕ Как уже отмечалось, слоты не являются виртуальными методами. Это значит, что хотя указатели на переменные класса ExtThread совместимы по присваиванию с указателями на QThread, попытка вызвать метод quit() после такого присваивания приведет к тому, что будет вызван метод quit() класса QThread, а он в нашем потоке не сработает. Методы start(), quit() и wait() выполняют в нашем классе те же действия, что и в классе QThread, хотя внутренне делают это немного иначе. Метод getCurrentState() возвращает текущее состояние потока:
Последний пункт списка требует некоторых пояснений. Состояние Finished устанавливается тогда, когда поток уже не выполняет никаких явных действий, но до того, как произойдет выход из процедуры потока. Это значит, что в момент установки состояния Finished некоторые объекты, автоматически созданные процедурой потока, могут все еще существовать. Методы pause() и resume() соответственно приостанавливают и возобновляют выполнение потока. Выполнение потока приостанавливается не сразу, а только тогда, когда процедура потока достигнет вызова CancellationPoint(). В момент фактического приостановления поток эмитирует сигнал paused(). Для возобновления выполнения потока необходимо вызвать метод resume(). Если на момент вызова resume() поток был приостановлен, он эмитирует сигнал resumed(). Вызов pause() не оказывает никакого воздействия на приостановленный поток, так же как и вызов resume() — на работающий. Среди методов, предназначенных для использования внутри потока, самый интересный — CancellationPoint(). Этот метод обрабатывает внешние команды, передаваемые потоку, такие как команда на завершение, приостановку или возобновление работы процедуры потока. Если на момент вызова CancellationPoint() поток не получал никаких команд, выполнение процедуры потока продолжается в обычном порядке. Сила метода CancellationPoint() заключается в том, что вы сами решаете, в каких участках процедуры потока вызывать этот метод. Вы можете разместить метод CancellationPoint() в тех точках, где поток может быть безопасно приостановлен или завершен. Если внешний поток использует корректный способ завершения потока ExtThread с помощью метода quit(), фактическое завершение произойдет в одной из точек, где будет вызван метод CancellationPoint(). Естественно, если вы забудете добавить вызовы CancellationPoint() в свой метод потока, поток завершится только тогда, когда его процедура завершится сама собой или когда будет вызван метод terminate(), что является очень неудачным способом завершения функции потока, особенно при программировании на C++. Выход из процедуры потока, написанной на C++, очень сильно отличается от выхода из процедуры потока, написанной на C. Язык C не создает автоматически никаких сложных структур данных, которые бы требовалось также автоматически уничтожить при выходе соответствующих переменных из области видимости. Если же вы создали такие структуры данных явно, то вы сможете явно их уничтожить перед выходом из процедуры потока (помните принцип "кто создавал, тот и удаляет"). В отличие от C, язык C++ может создавать структуры данных автоматически и уничтожать их автоматически при выходе из процедуры (например, при вызове оператора return ). Ключевые компоненты операционных систем пишутся на языке C, и практически во всех операционных системах есть функция досрочного выхода из потока. Но эта функция, как правило, не учитывает специфику C++ и по этой причине нам не подходит. Корректный выход из функции потока, написанной на C++, должен завершаться вызовом оператора return (явно или не явно, т. е. вместе со скобкой, обозначающей завершение функции). Этот факт нашел свое отражение в приемах работы с CancellationPoint(). Функция CancellationPoint() возвращает значение типа bool. Если функция обнаружила, что поток необходимо завершить, она возвращает true, в противном случае — false. Так что корректный вызов CancellationPoint() должен выглядеть так: if(CancellationPoint()) return; Метод done() выполняет действия, связанные с завершением работы потока. Этот метод нужно вызывать тогда, когда вы завершаете работу потока без участия CancellationPoint() (например, в конце процедуры потока). Функция CancellationPoint(), при необходимости, вызовет этот метод сама. Помимо прочего, метод done() вызывает виртуальный метод beforeQuit(), который вы можете переопределить в своем классе потока, если вам требуется выполнять какие-то особые действия перед завершением процедуры потока. Необходимость в таком методе возникает потому, что у процедуры потока есть только одна точка входа, но может быть много точек выхода. Метод beforeQuit() избавляет вас от необходимости дублировать код. Метод pauseFor() приостанавливает выполнение потока ExtThread на заданное количество миллисекунд. Перейдем теперь к реализации класса ExtThread (листинг 5.9). Листинг 5.9. Реализация класса ExtThread #include "extthread.h" #include <QAtomicInt> ExtThread::ExtThread(QObject *parent) : QThread(parent) { currentState = Ready; } bool ExtThread::CancellationPoint() { if (currentState == Finishing) { done(); return true; }
bool toSleep = false;
while (currentState == Sleeping) { if (!toSleep) { emit paused(); toSleep = true; } yieldCurrentThread(); }
if (toSleep) emit resumed();
if (currentState == Finishing) { done(); return true; }
return false; } bool ExtThread::pause() { bool res = currentState.testAndSetOrdered(Working, Sleeping); if (res) emit paused(); return res; } bool ExtThread::resume() { bool res = currentState.testAndSetOrdered(Sleeping, Working); if (res) emit resumed(); return res; } ExtThreadStates ExtThread::getCurrentState() { return (ExtThreadStates) (int) currentState; } bool ExtThread::wait(unsigned long time) { while (time > 0) { if (currentState == Finished){ QThread::wait(time); return true; } QThread::msleep(1); time--; if (currentState == Finished) { QThread::wait(time); return true; } } return false; } bool ExtThread::pauseFor(unsigned long milliseconds) { if (currentState.testAndSetOrdered(Working, Sleeping)) { while (milliseconds > 0) { QThread::sleep(1); milliseconds--; } }
return currentState.testAndSetOrdered(Sleeping, Working); } void ExtThread::quit() { currentState.testAndSetAcquire(Ready, Finished);
while ((!currentState.testAndSetOrdered(Working, Finishing))&& (!currentState.testAndSetOrdered(Sleeping, Finishing))&& (currentState != Finishing)&& (currentState!= Finished)); } void ExtThread::beforeQuit() { } void ExtThread::done() { beforeQuit(); currentState = Finished; emit finished(); } void ExtThread::start() { if (currentState.testAndSetOrdered(Ready, Working)||currentState.testAndSetOrdered(Finished, Working)) QThread::start(); } Для того чтобы разобраться в работе основных методов этого класса, необходимо понять один важный принцип, который имеет большое значение для многопоточного программирования вообще. Речь идет об атомарности операции "проверить и установить значение". Рассмотрим это на практическом примере. В методе resume() происходит примерно следующее: if (currentState == Sleeping) currentState = Working Эта операция состоит из двух элементарных операций: проверка текущего значения переменной currentState и (в зависимости от результатов проверки) установка нового значения. Даже если элементарные операции являются атомарными (а это не обязательно так), то вся строка, написанная выше, не является атомарной. То есть в интервале между if (currentState == Sleeping) и currentState = Working другой поток мог бы "вклиниться", например, с такой операцией: if (currentState == Sleeping) currentState = Finishing; В результате этой операции переменная currentState должна была бы получить значение Finishing, но по факту она все равно получила бы значение Working, чего другой поток мог бы и не знать. Этой неприятной ситуации можно избежать, если операции "проверить и установить значение" с переменной currentState сделать атомарными, т. е. такими, что когда один поток выполняет эту операцию, ни один другой поток не может выполнять ту же (или другую) операцию с теми же операндами до тех пор, пока данная операция не завершится. Современные микропроцессоры обладают специальными командами, позволяющими выполнять атомарные операции с простыми типами данных (например, с целыми числами или указателями). Кроме того существуют алгоритмы, с помощью которых можно реализовать такие операции в неатомарных системах. Для атомарных операций с целыми числами библиотека Qt library предоставляет нам класс QAtomicInt. В некоторых аспектах переменные типа QAtomicInt ведут себя как обычные целые числа. Но у класса QAtomicInt есть ряд методов, реализующих атомарные операции с этими числами. Метод testAndSetOrdered() представляет собой один из вариантов такой операции. Например, строка currentState.testAndSetOrdered(Sleeping, Working); эквивалентна строке if (currentState == Sleeping) currentState = Working; за исключением того, что другой поток не может "вклиниться" в выполнение этой операции. Метод testAndSetOrdered() возвращает значение типа bool, которое соответствует результату проверки. То есть, если метод возвращает true, присваивание состоялось, если false, — значит, нет. Насколько важен тип QAtomicInt и подобные ему типы в многопоточном программировании, можно продемонстрировать на следующем примере. Имея в своем распоряжении QAtomicInt, мы можем без труда реализовать мьютекс — один из важнейших примитивов синхронизации потоков (листинг 5.10). Листинг 5.10. Простой мьютекс на основе QAtomicInt class Mutex : public QObject { public: explicit Mutex(QObject *parent = 0) : QObject(parent) { m = 0; }
void lock() { while (!m.testAndSetOrdered(0, 1)) QThread::yieldCurrentThread(); }
bool tryLock() { return m.testAndSetOrdered(0, 1); }
void unlock() { m.testAndSetOrdered(1, 0); }
private: QAtomicInt m; }; Вызов метода lock() класса Mutex приводит к закрытию доступа в критическую область. После этого любой другой поток, который попытается вызвать lock(), будет заблокирован до тех пор, пока обладатель доступа в критическую область не вызовет метод unlock(). Метод tryLock() представляет собой попытку получить доступ в критическую область. Если какой-то другой поток получил этот доступ (т. е. вызвал lock() успешно), метод tryLock() вернет значение false, и это будет означать, что у потока, вызвавшего tryLock(), нет доступа в критическую область. Если же метод tryLock() вернет true, это значит, что данный поток получил доступ в критическую область и должен вызвать unlock(), когда закончит с ней работать. ПРИМЕЧАНИЕ Вы скажете, что другой поток может получить доступ в занятую критическую область, вызвав метод unlock() мьютекса. Конечно, это так. Впрочем, другой поток может войти в занятую критическую область, просто не вызвав метод lock(), поскольку такой вызов — единственное, что защищает критическую область от попадания в нее двух потоков. Вообще говоря, разделение доступа к критическим областям внутри одного приложения основано на добровольном согласии всех потоков играть по правилам. Действительная слабость предложенного мьютекса (который является просто демонстрационным примером) заключается в другом. Когда метод lock() блокирует потоку доступ в критическую область, мы хотим, чтобы заблокированный поток требовал как можно меньше процессорного времени. В идеале, чтобы вообще не требовал. Наш мьютекс в этом случае входит в цикл, в котором вызывается метод QThread::yieldCurrentThread(); который просто возвращает системе порцию машинного времени, изначально выделенную потоку. Это неплохой вариант холостого цикла, но не самый лучший. Самый лучший могут обеспечить низкоуровневые средства операционной системы. Другая проблема связана с исключительными ситуациями. В результате возникновения исключительной ситуации поток может выйти из критической области, не вызвав метод unlock(). Из-за этого другие потоки никогда не смогут войти в критическую область. Вот почему все же лучше пользоваться стандартными примитивами синхронизации, которые предоставляет нам Qt library. Кроме того, описанный выше вариант мьютекса делает функцию, использующую его, нереентерабельной. Атомарные операции и порядок доступа к памяти Современные процессоры могут, в целях оптимизации, изменить порядок операций доступа к памяти по сравнению с тем, как он представлен в программе. Очень часто нам требуется, чтобы определенная операция доступа к памяти была выполнена до или после атомарной операции, иначе сама атомарная операция потеряет смысл. Иными словами, при выполнении атомарных операций желательно, чтобы изменение порядка доступа к памяти не затрагивало саму операцию, т. е. все, что должно выполняться до атомарной операции, выполнялось бы до нее, независимо от взглядов компилятора и процессора на оптимизацию, а все операции доступа к памяти, которые в программе следуют за атомарной операцией, ни в коем случае не выполнялись до выполнения этой операции. У классов QAtomicInt и QAtomicPointer есть несколько вариантов метода TestAndSet* и других атомарных методов, которые поразному управляют изменением порядка доступа к памяти. Более подробные описания каждого метода вы найдете в документации. |
|||||||
Так же в этом разделе:
|
|||||||
|
|||||||
|