Threads Events QObjects/bg

From Qt Wiki
Jump to navigation Jump to search
This article may require cleanup to meet the Qt Wiki's quality standards. Reason: Auto-imported from ExpressionEngine.
Please improve this article if you can. Remove the {{cleanup}} tag and add this page to Updated pages list after it's clean.


Български English Русский 中文 한글

Нишки, Събития и QObject-и

Внимание: Вета версия

Тази статия е почти готова, но има нужда от още малко до изглаждане и няколко добри примера. Всякакъв вид ревюта или допълнения са добре дошли! Дискусия за статията може да намерите тук .

Българския превод ще го правя постепенно, защото статията е огромна.

Въведение

Правите го грешно. — Брадли Т. Хюджис

Една от най-популярните теми в "#qt IRC канала":irc://irc.freenode.net/#qt са нишките: Много хора влизат в канала и питат как да решат проблема си с код, който работи в нишка.

В девет от десет случая, бърз преглед на кода им, показва, че най-големият им проблем е, че въобще използват нишки, и те попадат на един от безкрайните капани на паралелното програмиране.

Лекотата, с която се създават и стартират нишки в Qt, комбинирана с липсата на знания за стила на програмиране (особено за асинхронно програмиране в мрежа, комбинирано със сигналите и слотовете в Qt) и/или навиците, придобити при използването на други библиотеки или езици, обикновено водят до "хора, които се прострелват сами в крака". Освен това, поддръжката на нишки в Qt е нож с две остриета: докато прави разработката на многонишкови програми много лесна, добавя и няколко възможности (особено, когато става дума за взаимодействието с QObject-и), за които трябва да сте наясно.

Целта на тази статия не е да ви научи как да използвате нишки, да правите правилно заключване, да разгледа паралелизма, или да пишете скалируеми програми; има много добри книги на тези теми; на пример, вижте препоръчителния списък за четене на тази страница. Вместо това, тази малка статия е предназначена да бъде пътеводител, който да въведе потребителите в нишките в Qt 4, с цел да избегнат най-честите капани и да им помогне да разработят своя код, така че той да бъде едновременно по-ясен и добре структуриран.

Предпоставки

Мислете по този начин: нишките са като солта, а не като тестените изделия.

Вие обичате сол, аз обичам сол, ние всички обичаме сол. Но ядем повече тестени изделия. — Лари МакВой

Тъй като това не е въведение с общо предназначение в програмирането (с нишки), се очаква да имате познания за:

  • Основите на C++ (въпреки че повечето предложения могат да бъдат приложени и в други езици);
  • Основи на Qt: QObject-и, сигнали и слотове, обработка на събития;
  • Какво е нишка и какви са взаимоотношенията между нишките, процесите и операционната система;
  • Как да стартирате и спирате нишка, и как да я изчакате да завърши, под (поне) една основна операционна система;
  • Как да използвате мутекси, семафори и изчакващи условия за да създавате безопасни за използване от няколко нишки/реентрантни функции, структури от данни и класове.

В тази статия ние следваме приетите в Qt наименования , които са:

  • Реентрантен Клас се счита за реентрантен, ако е безопасно да се използват негови инстанции от повече от една нишка, като най-много една нишка достъпва една инстанция в даден момент. Функция се счита за реентрантна, ако е безопасно да се извика от повече от една нишка едновременно, като е сигурно, че всяко извикване реферира уникални данни. С други думи, това значи, че потребителите на този клас/функция трябва да сериализира всеки достъп до инстанциите/споделените данни като се използва някакъв външен механизъм за заключване.
  • Нишково безопасен Клас се счита за нишково безопасен ако е безопасно да се използват неговите инстанции от повече от една нишка по едно и също време. Функция е нишково безопасна, ако е безопасно да се извика повече от една нишка в едно и също време дори и ако извикванията достъпват споделени данни.

Събития и цикъла на събитията

Поради това, че е Qt e система, задвижвана от събития, те и тяхното доставяне изграят централна роля в архитектурата на Qt. В тази статия няма да дадем подробно обяснение на тази тема; вместо това ние ще се фокусираме върху някои съврзани с нишките ключови концепции (вижте тук и тук за повече информация относно събитийната системата в Qt).

Събитие в Qt е обект, който представя нещо интересно, което се е случило; основната разлика между събитията и сигналите е, че събитията са насочени към специфичен обект в приложението (този обект решава какво да прави с това събитие), докато сигналите се излъчват "в дивото". От гледна точка на кода, всички събития са инстанции на подкласове на QEvent, и всички класове, наследяващи QObject, могат да презапишат виртуалния метод QObject::event() с цел да обработват събития, изпратени към техни инстанции.

Събитията могат да бъдат генерирани както от приложението, така и да дойдат някъде отвънка; на пример:

  • Обектите QKeyEvent и QMouseEvent представят някакво взаимодействие с клавиатурата и мишката, и те идват от мениджъра на прозорците;
  • Обектите от тип QTimerEvent се изпращат към QObject, когато на един от неговите таймери му изтече периода, и те (обикновено) идват от операционата система;
  • Обектите QChildEvent се изпращат, когато към QObject, когато е добавено или премахнато дете от него и тези събития идват от вашето Qt приложение.

Важно нещо за събитията е, че те не се доставят веднага щом са генерирани; вместо това те се добавят в опашка, известна като опашката на събития и се изпращат по-късно. Диспечера на събитията обикаля опашката и изпраща събраните събития към техните целеви обекти, ето за това се нарича цикъл на събитията. Концептуално, той изглежда по следния начин (вижте статията в Qt Quarterly, към която има връзка по-горе):

while (is_active)
{
 while (!event_queue_is_empty)
 dispatch_next_event();

wait_for_more_events();
}

Ние влизаме в главният цикъл на събитията в Qt, когато извикаме QCoreApplication::exec(); това извикване е блокиращо и изчаква, докато не се извика QCoreApplication::exit() или QCoreApplication::quit().

Функцията "wait_for_more_events()", блокира ( това не е изчакване защото нещо се пресмята ) докато не се генерира някоя събитие. Ако се замислите, всичко, което може генерира събития на този етап е от някакъв външен източник (разпращането на всички вътрешни събития е свършило и няма повече изчакващи на опашката). Следователно, цикъла на събитията може да се събуди от:

  • активност от мениджъра на прозорците (натискане на клавиш/бутон на мишката, взаимодействие с другите прозорци и т.н.);
  • активност от мрежата (има нови данни за четене, или може да се пише в някой сокет без блокиране, или има нова връзка и т.н.);
  • таймери ( примерно на някой таймер му е изтекъл периода);
  • събития създадени от други нишки (ще го разгледаме по-късно).

В UNIX подобните системи, активност на мениджъра на прозорците (примерно X11) се съобщава на приложенията посредством сокети (Unix Domain или TCP/IP), тъй като клиентите ги използват за комуникация с X сървъра. Ако ние решим да разработим междунишково пускане на събития с вътрешен socketpair(2), всичко, което остава е да се будим от активност на:

  • сокети;
  • таймери;

което е точно това, което прави системното извикване select(2): то следи множество от дескриптори за активност и прекратява изпълнението си след известно време (то може да се конфигурира), ако няма никаква активност. Всичко, което Qt трябва да направи е да преработи това, което select връща, в обекти от подкласове на QEvent и да ги добави в опашката на събитията. Сега вече знаете какво има в цикъла на събитията :)

Какво изисква работещ цикъл на събитията?

Това не е изчерпателен списък, но ако разбирате цялата картина, трябва да сте в състояние да познаете кои класове изискват работещ цикъл на събитията.

  • Прерисуване на графични елементи и взаимодействие с тях: QWidget::paintEvent() ще бъде извикан, когато се получат QPaintEvent обекти, които може да са генерирани както от извикването на QWidget::update() (т.е вътрешно) така и от мениджъра на прозорците (на пример, когато скрит прозорец трябва да бъде показан отново). Същото нещо важи за всички видове взаимодействия (клавиатура, мишка и т.н.): съответните събития изискват работещ цикъл за да бъдат обработени.
  • Таймери: казано на кратко, те пускат събитие, когато select(2) или на подобно извикване, му изтече времето, следователно трябва да позволите на Qt да обработи тези събития.
  • Работа в мрежа: всички Qt мрежови класове от ниско ниво (QTcpSocket, QUdpSocket, QTcpServer и т.н.) са асинхронни. Когато извикате read(), те просто връщат дошлите до сега данни; когато извикате write(), те оставят писането за по-късно. Така, че реалното четене/записване става чак, когато се завърнете в цикъла на събитията. Забележете, че тези класове предлагат и синхронни методи(семейството от методи, започващи с waitFor* ), но употребата им не е препоръчителна, защото те блокират цикъла на събитията, докато чакат за данните. Класовете от високо ниво, като QNetworkAccessManager, просто не предлагат синхронно API и изискват да има цикъл.

Блокиране на цикъла на събитията

Преди да обсъдим защо никога не трябва да блокирате цикъла на събитията, нека да опитаме да установим какво значи "блокиране". Нека да предположим, че имате бутон, който излъчва сигнал, когато е натиснат; има и слот на обекта Worker, който е свързан към този сигнал, и този слот върши много работа. След като натиснете бутона, стека с извиканите функции ще изглежда ето така (стека расте надолу):

  1. main(int, char )
  2. QApplication::exec()
  3. […]
  4. QWidget::event(QEvent )
  5. Button::mousePressEvent(QMouseEvent)
  6. Button::clicked()
  7. […]
  8. Worker::doWork()

В main() ние стартираме цикъла на събитията, обикновено, като извикаме QApplication::exec() (линия 2). Мениджъра на прозорците ни изпраща съобщение за натискане на мишката, което е прихванато от ядрото на Qt, конвертирано в QMouseEvent и изпратено на метода event() на нашия бутон (линия 4) от QApplication::notify() (не е показано тук). Тъй като Button не презаписва event(), се извиква имплементацията на базовият клас (QWidget). QWidget::event() разпознава събитието като натискане на мишката и извиква специализирания метод за обработка Button::mousePressEvent() (линия 5). Ние сме презаписали този метод да изпраща сигнала Button::clicked() (линия 6), което извиква слота Worker::doWork на нашия Worker обект (линия 7).

Докато Worker е зает да работи, какво прави цикъла на събитията? Трябва да сте предположили: нищо! Той е предал събитието за натискане на мишката и е блокирал, чакайки метода за обработка да свърши. Ние успяхме да блокираме цикъла, което значи, че повече няма да се изпращат събития, докато ние не излезем от слота doWork(), нагоре по стека, чак до цикъла и да му позволим да обработи новодошлите събития.

Със забило доставяне на събитията, графичните елементи няма да могат да се прерисуват (QPaintEvent обектите ще седят в опашката), няма да може да има по нататъшно взаимодействие с тях (поради същата причина), таймерите няма да могат да изпращат сигнали, че им е изтекло времето и мрежовата комуникация ще се забави и спре. Освен това, много от мениджърите на прозорци ще засекат, че вашето приложение вече не обработва събития и ще кажат на потребителя, че приложението ви не отговаря. Ето за това е толкова важно бързо да реагирате на събития и да се завръщате в цикъла на събития възможно най-бързо!

Принудителна обработка на събития

Така… Какво можем да направим, ако имаме дълга задача и не искаме да блокираме цикъла на събитията? Единият възможен отговор е да сложим задачата в друга нишка( в следващите секции ще разгледаме как става това). Също така имаме възможност ръчно да накараме цикъла да се стартира като (нееднократно) извикваме CoreApplication::processEvents() вътре в нашата функция. QCoreApplication::processEvents() ще обработи събитията в опашката и след това ще продължи изпълнението на кода ви.

Друг възможен начин е чрез класа QEventLoop. Извиквайки QEventLoop::exec(), можем да влезем в цикъла на събитията и да свържем сигнали към слота QEventLoop::quit(). На пример:

QNetworkAccessManager qnam;
QNetworkReply '''reply = qnam.get(QNetworkRequest(QUrl()));
QEventLoop loop;
QObject::connect(reply, SIGNAL (finished()), &loop, SLOT (quit()));
loop.exec();
/''' отговора е готов, използвате го */

QNetworkReply не предоставя блокиращо API и изисква цикъл за да работи. Ние влизаме в локален QEventLoop, и когато отговора е готов, напускаме цикъла.

Бъдете много внимателни, когато влизате на ново в цикъла на събитията "от други пътища": това може да доведе до нежелана рекурсия! Нека да се върнем на примера с бутона. Ако извикваме QCoreApplication::processEvents() вътре в слота doWork() и потребителя натисне пак бутона, doWork() ще бъде извикан отново:

  1. main(int, char)
  2. QApplication::exec()
  3. […]
  4. QWidget::event(QEvent )
  5. Button::mousePressEvent(QMouseEvent)
  6. Button::clicked()
  7. […]
  8. Worker::doWork() // първо извикване
  9. QCoreApplication::processEvents() // ръчно обработваме събитията и…
  10. […]
  11. QWidget::event(QEvent * ) // друго натискане на мишката е изпратено на бутона Button…
  12. Button::mousePressEvent(QMouseEvent *)
  13. Button::clicked() // което изпраща сигнала clicked() отново…
  14. […]
  15. Worker::doWork() // ОПА! отново се извиква нашия слот в самия себе си.

Бързо и лесно заобиколно решение на този проблем е да подадем QEventLoop::ExcludeUserInputEvents на QCoreApplication::processEvents(). То казва на цикъла да не обработва събития, свързани с вход от потребителя(тези събития ще останат в опашката за по-късно).

За щастие, това не важи за събитията за изтриване (тези публикувани в цикъла от QObject::deleteLater()). В същност, те се прихващат по специален начин от Qt, и се обработват само ако работещия цикъл на събитията има по ниско ниво на "вложеност" (по отношение на цикли на събития) от това, в което е било извикано deleteLater. За пример:

QObject *object = new QObject;
object->deleteLater();
QDialog dialog;
dialog.exec();

няма да направи object невалиден указател ( цикъла, в който се влиза от QDialog::exec() има по-голяма вложеност от извикването на deleteLater). Същото нещо важи и за локалните цикли, стартирани с QEventLoop. Единственото забележимо изключение, което намерих, на това правило (по време на Qt 4.7.3) е, че ако deleteLater е извикан, когато няма работещ цикъл, то при първото влизане в такъв, събитието ще бъде обработено и обекта ще бъде изтрит. В това има доста смисъл, тъй като Qt не знае за друг "по-външен" цикъл, който евентуално да извърши изтриването и следователно изтрива обекта моментално.

.

Нишкови класове в Qt

Компютъра е стейт машина. Нишките са за хора, които не могат да програмират стейт машини.

— Алън Кокс

Qt има поддръжка на нишки от много години (Qt 2.2, пуснато на 22 Sept 2000, представи класа QThread.), а от 4.0 нишките са включени по подразбиране на всички поддържани платформи (въпреки, че могат да бъдат спрени. Вижте тук за повече детайли). В момента Qt предлага няколко класа за работа с нишки. Нека да започнем с общ преглед.

QThread

QThread е централен клас от ниско ниво за поддръжка на нишки в Qt. Обект от тип QThread представя една нишка. Поради многоплатформеността на Qt, QThread успява да скрие платформено зависимия код, който е нужен за използването на нишки в различните операционни системи.

За да използваме QThread за стартиране на код в нишка, ние можем да му направим подклас и да пренапишем метода QThread::run():

class Thread : public QThread {
protected:
 void run() {
 /* имплементацията на вашата нишка */
 }
};

Тогава може да използваме

Thread '''t = new Thread;
t->start(); // start(), не run()!

за да пуснем новата нишка. Забележете, че от Qt 4.4, QThread вече не е абстрактен клас; сега виртуалния метод QThread::run() просто извиква QThread::exec();, която пуска цикъла на събитията на нишката (повече информация за това по-късно).


QRunnable и QThreadPool

QRunnable е лек абстрактен клас, който може да се използва, за да се пусне задача в друга нишка в стила "пусни и забрави". За да постигнете това, всичко, което трябва да направите е да създадете подклас на QRunnable и да имплементирате неговия чисто виртуален метод run():

class Task : public QRunnable {
public:
 void run() {
 /''' вашият код седи тук */
 }
};

За да пуснем QRunnable обект, ние използваме класа QThreadPool , който управлява множество от нишки. Чрез извикването на QThreadPool::start(runnable), ние добавяме QRunnable в опашката за изпълнение на QThreadPool; веднага щом нишката е на разположение, QRunnable-а ще бъде взет и пуснат на нея. Всички Qt приложения имат на разположение глобално множество от нишки, което е достъпно чрез QThreadPool::globalInstance(), но винаги могат да създадат и лична инстанция на QThreadPool и да я управляват самостоятелно.

Забележете, че тъй като не е QObject, QRunnable няма вграден начин за комуникация с другите компоненти. Вие трябва да направите това ръчно, използвайки ниските нива за работа с нишки (на пример защитена с мутекси опашка за събиране на резултатите).

QtConcurrent

QtConcurrent е API от по-високо ниво, построено върху QThreadPool, полезно за работа с най-често срещаните паралелни компютърни шаблони: map), reduce), и filter) . Също така предоставя и метода QtConcurrent::run(), чрез който може лесно да се пусне функция в отделна нишка.

За разлика от QThread и QRunnable, QtConcurrent не изисква от нас да използваме синхронизация от ниско ниво - всички методи на QtConcurrent връщат QFuture обект, който може да се използва за взимане на статуса на изпълнението(неговия прогрес), да прекъсва временно/стартира отново/прекратява изчислението, а също така и съдържа неговите резултати. Класа QFutureWatcher може да се използва за да се следи прогреса на QFuture и да се взаимодейства с него чрез сигнали и слотове (забележете, че QFuture, бидейки клас базиран на стойност, не наследява QObject).

Сравнение на възможностите

QThread QRunnable QtConcurrent[1]
High level API
Job-oriented
Builtin support for pause/resume/cancel
Can run at a different priority
Can run an event loop

Нишки и QObject-и

Цикъл на събитията за всяка нишка

До сега ние говорихме за "цикълът на събитията", взимайки някак си за даденост, че има само един такъв цикъл в Qt приложенията. Но това не е така: QThread обектите могат да стартират локални за нишката цикли на събитията, които да работят в нея. За това казваме, че главният цикъл на събитията е този, създаден от нишката, извикана в main() и стартирана с QCoreApplication::exec() (който трябва да бъде извикан от тази нишка). Това също така се нарича нишка на потребителската графика (GUI thread), защото е единствената нишка, в която е позволено да има операции, свързани с графичните елементи. Локалният цикъл на събитията за QThread може да се стартира като се извика QThread::exec() (вътре в нейният run() метод):

class Thread : public QThread {
protected:
 void run() {
 /* … инициализиране … */

exec();
 }
};

Както споменахме по-горе, от Qt 4.4 QThread::run() вече не е чисто виртуален метод; вместо това, той извиква QThread::exec(). Точно както QCoreApplication, QThread също има QThread::quit() и QThread::exit() методи за спиране на цикъла.

Нишковият цикъл доставя събитията за всички QObject-и, които живеят в тази нишка. Това включва, по подразбиране, всички обекти, създадени в нишката или такива преместени от друга (повече информация за това по-късно). Също така казваме, че нишковият афинитет на QObject е дадена нишка, когато този обект живее в нея. Това важи за обекти, които са създадени в конструктора на QThread обект:

class MyThread : public QThread
{
public:
 MyThread()
 {
 otherObj = new QObject;
 }

private:
 QObject obj;
 QObject *otherObj;
 QScopedPointer<QObject> yetAnotherObj;
};

Какъв е афинитета на obj, otherObj, yetAnotherObj след като създадем обект от тип MyThread? Трябва да гледаме нишката, която ги е създала: тя е тази, която е изпълнила конструктора на MyThread. Следователно и трите обекта не живеят в нишката MyThread, а в тази, която е създала инстанцията на MyThread ( инстанцията също живее в нея).

Ние може да видим афинитета към нишка на QObject като извикаме QObject::thread(). Забележете, че QObject-ите, създадени преди QCoreApplication обекта нямат нишков афинитет, и следователно за тях няма да има разпределяне на събитията (с други думи, QCoreApplication създава обекта от тип QThread, който представлява главната нишка).

http://doc.qt.nokia.com/4.7/images/threadsandobjects.png

Ние може да използваме нишково безопасният метод QCoreApplication::postEvent() за пускане на събитие за даден обект. Това ще добави събитието в опашката на цикъла на събитията за нишката, в която съществува обекта; следователно, събитието няма да се обработи, освен ако нишката няма работещ цикъл.

Много е важно да разберете, че QObject и всички негови подкласове не са нишково безопасни (въпреки, че те могат да бъдат реентрантни); следователно, не можете да достъпвате QObject от повече от една нишка по едно и също време, освен ако не сериализирате достъпа до вътрешните данни на обекта (на пример като го защитите с мутекс). Запомнете, че обекта може да обработва събития, изпратени от цикъла на събитията на нишката, в която съществува, докато вие го достъпвате от друга нишка! По същата причина, не можете да изтриете QObject от друга нишка, но можете да използвате QObject::deleteLater(), която ще създаде събитие, което ще предизвика унищожаването на обекта от нишката, в която съществува.

Освен това QWidget и всички негови подкласове, заедно с други класове, свързани с графичния интерфейс (дори не базираните на QObject като QPixmap) не са реентрантни: те могат да се използват само от графичната нишка.

Можем да променим афинитета на QObject като извикаме QObject::moveToThread(). Това ще промени неговият афинитет и този на неговите деца. Тъй като QObject не е нишково безопасен, ние трябва да го използваме от нишката, в която съществува. За това, вие можете само да премествате обект от нишката, в която е, към друга нишка, но не и да го взимате от друга или да го местите между две нишки, от които никоя не е тази, в която съществува. Освен това, Qt изисква децата на QObject да са в същата нишка, в която е и той. Това означава, че:

  • не можете да използвате QObject::moveToThread() с обект, който има родител;
  • не трябва да създавате обекти в дадена нишка, които имат за родител QThread-a създал нишката:
class Thread : public QThread {
 void run() {
 QObject obj = new QObject(this); // ГРЕШНО[[Image:|Image:]]!
 }
};

Това е защото QThread обекта съществува в друга нишка, а именно, тази, която го е създала.

Qt също изисква всички обекти, съществуващи в дадена нишка, да се изтриват преди QThread обекта, представляващ тази нишка, да бъде унищожен. Това може да се направи лесно като всички обекти, съществуващи в нишката, се създадат статично в стека на метода QThread::run().

Сигнали и слотове между нишките

Предвид казаното до тук, как да извикваме методи на QObject-и, съществуващи в други нишки? Qt предлага много приятно и чисто решение: Ние създаваме събитие в опашката със събития за тази нишка, и обработването на това събитие ще се състои в извикването на метода, който ни интересува (това, разбира се, изисква нишката да има работещ цикъл на събитията). Тази функционалност е изградена около анализа на методите, предоставен от moc: следователно само сигнали, слотове и методи, маркирани с макроса Q_INVOKABLE, могат да бъдат извикани от други нишки.

Статичният метод QMetaObject::invokeMethod() върши цялата работа вместо нас:

QMetaObject::invokeMethod(object, "methodName",

Qt::QueuedConnection,
Q_ARG(type1, arg1),
Q_ARG(type2, arg2));
  1. С изключение на QtConcurrent::run, който е реализиран като се използва QRunnable и следователно споделя неговите преимущества и недостатъци.