Threads Events QObjects/bg: Difference between revisions

From Qt Wiki
Jump to navigation Jump to search
No edit summary
 
No edit summary
Line 1: Line 1:
'''Български''' [[Threads Events QObjects|English]] [[Threads-Events-QObjects Russian|Русский]] [[Threads-Events-QObjects Chinese|中文]] [[Threads Events QObjects Korean|한글]]
[toc align_right="yes" depth="2"]


=Нишки, Събития и QObject-и=
'''Български''' [[Threads_Events_QObjects|English]] [[Threads Events QObjects_Russian|Русский]] [[Threads Events QObjects_Chinese|中文]] [[Threads_Events_QObjects_Korean|한글]]


==Внимание: Вета версия==
= Нишки, Събития и QObject-и =


Тази статия е почти готова, но има нужда от още малко до изглаждане и няколко добри примера. Всякакъв вид ревюта или допълнения са добре дошли! Дискусия за статията може да намерите [http://developer.qt.nokia.com/forums/viewthread/2423/ тук] ''[developer.qt.nokia.com]'' .
== Внимание: Вета версия ==
 
Тази статия е почти готова, но има нужда от още малко до изглаждане и няколко добри примера. Всякакъв вид ревюта или допълнения са добре дошли! Дискусия за статията може да намерите "тук":http://developer.qt.nokia.com/forums/viewthread/2423/ .


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


=Въведение=
= Въведение =
 
<blockquote>
 
Правите го грешно. — Брадли Т. Хюджис


<blockquote>Правите го грешно. — Брадли Т. Хюджис
</blockquote>
</blockquote>
 
Една от най-популярните теми в &quot;#qt IRC канала&amp;quot;:irc://irc.freenode.net/#qt са нишките: Много хора влизат в канала и питат как да решат проблема си с код, който работи в нишка.
Една от най-популярните теми в [irc://irc.freenode.net/#qt #qt <span class="caps">IRC</span> канала] ''[irc.freenode.net]'' са нишките: Много хора влизат в канала и питат как да решат проблема си с код, който работи в нишка.


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


Лекотата, с която се създават и стартират нишки в Qt, комбинирана с липсата на знания за стила на програмиране (особено за асинхронно програмиране в мрежа, комбинирано със сигналите и слотовете в Qt) и/или навиците, придобити при използването на други библиотеки или езици, обикновено водят до “хора, които се прострелват сами в крака”. Освен това, поддръжката на нишки в Qt е нож с две остриета: докато прави разработката на многонишкови програми много лесна, добавя и няколко възможности (особено, когато става дума за взаимодействието с QObject-и), за които трябва да сте наясно.
Лекотата, с която се създават и стартират нишки в Qt, комбинирана с липсата на знания за стила на програмиране (особено за асинхронно програмиране в мрежа, комбинирано със сигналите и слотовете в Qt) и/или навиците, придобити при използването на други библиотеки или езици, обикновено водят до &quot;хора, които се прострелват сами в крака&amp;quot;. Освен това, поддръжката на нишки в Qt е нож с две остриета: докато прави разработката на многонишкови програми много лесна, добавя и няколко възможности (особено, когато става дума за взаимодействието с QObject-и), за които трябва да сте наясно.
 
Целта на тази статия '''не е''' да ви научи как да използвате нишки, да правите правилно заключване, да разгледа паралелизма, или да пишете скалируеми програми; има много добри книги на тези теми; на пример, вижте препоръчителния списък за четене [http://doc.qt.nokia.com/latest/threads.html на тази страница] ''[doc.qt.nokia.com]''. Вместо това, тази малка статия е предназначена да бъде пътеводител, който да въведе потребителите в нишките в Qt 4, с цел да избегнат най-честите капани и да им помогне да разработят своя код, така че той да бъде едновременно по-ясен и добре структуриран.
 
==Предпоставки==


<blockquote>
Целта на тази статия '''не е''' да ви научи как да използвате нишки, да правите правилно заключване, да разгледа паралелизма, или да пишете скалируеми програми; има много добри книги на тези теми; на пример, вижте препоръчителния списък за четене &quot;на тази страница&amp;quot;:http://doc.qt.nokia.com/latest/threads.html. Вместо това, тази малка статия е предназначена да бъде пътеводител, който да въведе потребителите в нишките в Qt 4, с цел да избегнат най-честите капани и да им помогне да разработят своя код, така че той да бъде едновременно по-ясен и добре структуриран.


Мислете по този начин: нишките са като солта, а не като тестените изделия.<br /> Вие обичате сол, аз обичам сол, ние всички обичаме сол. Но ядем повече тестени изделия.<br /> — Лари МакВой
== Предпоставки ==


<blockquote>Мислете по този начин: нишките са като солта, а не като тестените изделия.<br />Вие обичате сол, аз обичам сол, ние всички обичаме сол. Но ядем повече тестени изделия.<br />— Лари МакВой
</blockquote>
</blockquote>
Тъй като това не е въведение с общо предназначение в програмирането (с нишки), се очаква да имате познания за:
Тъй като това не е въведение с общо предназначение в програмирането (с нишки), се очаква да имате познания за:


Line 41: Line 35:
* Как да използвате мутекси, семафори и изчакващи условия за да създавате безопасни за използване от няколко нишки/реентрантни функции, структури от данни и класове.
* Как да използвате мутекси, семафори и изчакващи условия за да създавате безопасни за използване от няколко нишки/реентрантни функции, структури от данни и класове.


В тази статия ние следваме [http://doc.qt.nokia.com/latest/threads-reentrancy.html приетите в Qt наименования] ''[doc.qt.nokia.com]'' , които са:
В тази статия ние следваме &quot;приетите в Qt наименования&amp;quot;:http://doc.qt.nokia.com/latest/threads-reentrancy.html , които са:


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


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


Поради това, че е Qt e система, задвижвана от събития, те и тяхното доставяне изграят централна роля в архитектурата на Qt. В тази статия няма да дадем подробно обяснение на тази тема; вместо това ние ще се фокусираме върху някои съврзани с нишките ключови концепции (вижте [http://doc.qt.nokia.com/latest/eventsandfilters.html тук] ''[doc.qt.nokia.com]'' и [http://doc.qt.nokia.com/qq/qq11-events.html тук] ''[doc.qt.nokia.com]'' за повече информация относно събитийната системата в Qt).
Поради това, че е Qt e система, задвижвана от събития, те и тяхното доставяне изграят централна роля в архитектурата на Qt. В тази статия няма да дадем подробно обяснение на тази тема; вместо това ние ще се фокусираме върху някои съврзани с нишките ключови концепции (вижте &quot;тук&amp;quot;:http://doc.qt.nokia.com/latest/eventsandfilters.html и &quot;тук&amp;quot;:http://doc.qt.nokia.com/qq/qq11-events.html за повече информация относно събитийната системата в Qt).


'''Събитие''' в Qt е обект, който представя нещо интересно, което се е случило; основната разлика между събитията и сигналите е, че събитията са ''насочени'' към специфичен обект в приложението (този обект решава какво да прави с това събитие), докато сигналите се излъчват “в дивото”. От гледна точка на кода, всички събития са инстанции на подкласове на [http://doc.qt.nokia.com/latest/qevent.html QEvent] ''[doc.qt.nokia.com]'', и всички класове, наследяващи QObject, могат да презапишат виртуалния метод QObject::event() с цел да обработват събития, изпратени към техни инстанции.
'''Събитие''' в Qt е обект, който представя нещо интересно, което се е случило; основната разлика между събитията и сигналите е, че събитията са ''насочени'' към специфичен обект в приложението (този обект решава какво да прави с това събитие), докато сигналите се излъчват &quot;в дивото&amp;quot;. От гледна точка на кода, всички събития са инстанции на подкласове на &quot;QEvent&amp;quot;:http://doc.qt.nokia.com/latest/qevent.html, и всички класове, наследяващи QObject, могат да презапишат виртуалния метод QObject::event() с цел да обработват събития, изпратени към техни инстанции.


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


* Обектите QKeyEvent и QMouseEvent представят някакво взаимодействие с клавиатурата и мишката, и те идват от мениджъра на прозорците;
Важно нещо за събитията е, че те не се доставят веднага щом са генерирани; вместо това те се добавят в опашка, известна като '''опашката на събития''' и се изпращат по-късно. Диспечера на събитията обикаля опашката и изпраща събраните събития към техните целеви обекти, ето за това се нарича '''цикъл на събитията'''. Концептуално, той изглежда по следния начин (вижте статията в Qt Quarterly, към която има връзка по-горе):<br /><code><br />while (is_active)<br />{<br /> while (!event_queue_is_empty)<br /> dispatch_next_event();
* Обектите от тип QTimerEvent се изпращат към QObject, когато на един от неговите таймери му изтече периода, и те (обикновено) идват от операционата система;
* Обектите QChildEvent се изпращат, когато към QObject, когато е добавено или премахнато дете от него и тези събития идват от вашето Qt приложение.


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


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


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


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


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


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


което е точно това, което прави системното извикване '''select(2)''': то следи множество от дескриптори за активност ''и'' прекратява изпълнението си след известно време (то може да се конфигурира), ако няма никаква активност. Всичко, което Qt трябва да направи е да преработи това, което ''select'' връща, в обекти от подкласове на QEvent и да ги добави в опашката на събитията. Сега вече знаете какво има в цикъла на събитията <span class="smiley">:)</span>
== Какво изисква работещ цикъл на събитията? ==
 
==Какво изисква работещ цикъл на събитията?==


Това не е изчерпателен списък, но ако разбирате цялата картина, трябва да сте в състояние да познаете кои класове изискват работещ цикъл на събитията.
Това не е изчерпателен списък, но ако разбирате цялата картина, трябва да сте в състояние да познаете кои класове изискват работещ цикъл на събитията.
Line 82: Line 71:
* '''Прерисуване на графични елементи и взаимодействие с тях''': QWidget::paintEvent() ще бъде извикан, когато се получат QPaintEvent обекти, които може да са генерирани както от извикването на QWidget::update() (т.е вътрешно) така и от мениджъра на прозорците (на пример, когато скрит прозорец трябва да бъде показан отново). Същото нещо важи за всички видове взаимодействия (клавиатура, мишка и т.н.): съответните събития изискват работещ цикъл за да бъдат обработени.
* '''Прерисуване на графични елементи и взаимодействие с тях''': QWidget::paintEvent() ще бъде извикан, когато се получат QPaintEvent обекти, които може да са генерирани както от извикването на QWidget::update() (т.е вътрешно) така и от мениджъра на прозорците (на пример, когато скрит прозорец трябва да бъде показан отново). Същото нещо важи за всички видове взаимодействия (клавиатура, мишка и т.н.): съответните събития изискват работещ цикъл за да бъдат обработени.
* '''Таймери''': казано на кратко, те пускат събитие, когато select(2) или на подобно извикване, му изтече времето, следователно трябва да позволите на Qt да обработи тези събития.
* '''Таймери''': казано на кратко, те пускат събитие, когато select(2) или на подобно извикване, му изтече времето, следователно трябва да позволите на Qt да обработи тези събития.
* '''Работа в мрежа''': всички Qt мрежови класове от ниско ниво (QTcpSocket, QUdpSocket, QTcpServer и т.н.) са асинхронни. Когато извикате read(), те просто връщат дошлите до сега данни; когато извикате write(), те оставят писането за по-късно. Така, че реалното четене/записване става чак, когато се завърнете в цикъла на събитията. Забележете, че тези класове предлагат и синхронни методи(семейството от методи, започващи с waitFor* ), но употребата им не е препоръчителна, защото те блокират цикъла на събитията, докато чакат за данните. Класовете от високо ниво, като QNetworkAccessManager, просто не предлагат синхронно <span class="caps">API</span> и изискват да има цикъл.
* '''Работа в мрежа''': всички Qt мрежови класове от ниско ниво (QTcpSocket, QUdpSocket, QTcpServer и т.н.) са асинхронни. Когато извикате read(), те просто връщат дошлите до сега данни; когато извикате write(), те оставят писането за по-късно. Така, че реалното четене/записване става чак, когато се завърнете в цикъла на събитията. Забележете, че тези класове предлагат и синхронни методи(семейството от методи, започващи с waitFor* ), но употребата им не е препоръчителна, защото те блокират цикъла на събитията, докато чакат за данните. Класовете от високо ниво, като QNetworkAccessManager, просто не предлагат синхронно API и изискват да има цикъл.
 
==Блокиране на цикъла на събитията==
 
Преди да обсъдим защо '''никога не трябва да блокирате цикъла на събитията''', нека да опитаме да установим какво значи “блокиране”. Нека да предположим, че имате бутон, който излъчва сигнал, когато е натиснат; има и слот на обекта Worker, който е свързан към този сигнал, и този слот върши много работа. След като натиснете бутона, стека с извиканите функции ще изглежда ето така (стека расте надолу):
 
# main(int, char **)
# QApplication::exec()
# […]
# QWidget::event(QEvent *)
# Button::mousePressEvent(QMouseEvent *)
# Button::clicked()
# […]
# 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() ще обработи събитията в опашката и след това ще продължи изпълнението на кода ви.
 
Друг възможен начин е чрез класа [http://doc.qt.nokia.com/latest/qeventloop.html QEventLoop] ''[doc.qt.nokia.com]''. Извиквайки QEventLoop::exec(), можем да влезем в цикъла на събитията и да свържем сигнали към слота QEventLoop::quit(). На пример:


QNetworkReply не предоставя блокиращо <span class="caps">API</span> и изисква цикъл за да работи. Ние влизаме в локален QEventLoop, и когато отговора е готов, напускаме цикъла.
== Блокиране на цикъла на събитията ==


Бъдете много внимателни, когато влизате на ново в цикъла на събитията “от други пътища”: това може да доведе до нежелана рекурсия! Нека да се върнем на примера с бутона. Ако извикваме QCoreApplication::processEvents() вътре в слота doWork() и потребителя натисне пак бутона, doWork() ще бъде извикан '''отново''':
Преди да обсъдим защо '''никога не трябва да блокирате цикъла на събитията''', нека да опитаме да установим какво значи &quot;блокиране&amp;quot;. Нека да предположим, че имате бутон, който излъчва сигнал, когато е натиснат; има и слот на обекта Worker, който е свързан към този сигнал, и този слот върши много работа. След като натиснете бутона, стека с извиканите функции ще изглежда ето така (стека расте надолу):


# main(int, char **)
# main(int, char ''')<br /># QApplication::exec&amp;amp;#40;&amp;#41;<br /># […]<br /># QWidget::event(QEvent ''')<br /># Button::mousePressEvent(QMouseEvent''')<br /># Button::clicked()<br /># […]<br /># Worker::doWork()  
# QApplication::exec()
<br />В main() ние стартираме цикъла на събитията, обикновено, като извикаме QApplication::exec&amp;amp;#40;&amp;#41; (линия 2). Мениджъра на прозорците ни изпраща съобщение за натискане на мишката, което е прихванато от ядрото на Qt, конвертирано в QMouseEvent и изпратено на метода event() на нашия бутон (линия 4) от QApplication::notify() (не е показано тук). Тъй като Button не презаписва event(), се извиква имплементацията на базовият клас (QWidget). QWidget::event() разпознава събитието като натискане на мишката и извиква специализирания метод за обработка Button::mousePressEvent() (линия 5). Ние сме презаписали този метод да изпраща сигнала Button::clicked() (линия 6), което извиква слота Worker::doWork на нашия Worker обект (линия 7).
<br />Докато Worker е зает да работи, какво прави цикъла на събитията? Трябва да сте предположили: нищо! Той е предал събитието за натискане на мишката и е блокирал, чакайки метода за обработка да свърши. Ние успяхме да '''блокираме цикъла''', което значи, че повече няма да се изпращат събития, докато ние не излезем от слота doWork(), нагоре по стека, чак до цикъла и да му позволим да обработи новодошлите събития.
<br />Със забило доставяне на събитията, '''графичните елементи няма да могат да се прерисуват''' (QPaintEvent обектите ще седят в опашката), '''няма да може да има по нататъшно взаимодействие с тях''' (поради същата причина), '''таймерите няма да могат да изпращат сигнали, че им е изтекло времето''' и '''мрежовата комуникация ще се забави и спре'''. Освен това, много от мениджърите на прозорци ще засекат, че вашето приложение вече не обработва събития и '''ще кажат на потребителя, че приложението ви не отговаря'''. Ето за това е толкова важно бързо да реагирате на събития и да се завръщате в цикъла на събития възможно най-бързо!
<br />h2. Принудителна обработка на събития
<br />Така… Какво можем да направим, ако имаме дълга задача и не искаме да блокираме цикъла на събитията? Единият възможен отговор е да сложим задачата в друга нишка( в следващите секции ще разгледаме как става това). Също така имаме възможност ръчно да накараме цикъла да се стартира като (нееднократно) извикваме CoreApplication::processEvents() вътре в нашата функция. QCoreApplication::processEvents() ще обработи събитията в опашката и след това ще продължи изпълнението на кода ви.
<br />Друг възможен начин е чрез класа &quot;QEventLoop&amp;quot;:http://doc.qt.nokia.com/latest/qeventloop.html. Извиквайки QEventLoop::exec&amp;amp;#40;&amp;#41;, можем да влезем в цикъла на събитията и да свържем сигнали към слота QEventLoop::quit(). На пример:
<br /><code><br />QNetworkAccessManager qnam;<br />QNetworkReply '''reply = qnam.get(QNetworkRequest(QUrl(…)));<br />QEventLoop loop;<br />QObject::connect(reply, SIGNAL (finished()), &amp;loop, SLOT (quit()));<br />loop.exec&amp;amp;#40;&amp;#41;;<br />/''' отговора е готов, използвате го */<br /></code>
<br />QNetworkReply не предоставя блокиращо API и изисква цикъл за да работи. Ние влизаме в локален QEventLoop, и когато отговора е готов, напускаме цикъла.
<br />Бъдете много внимателни, когато влизате на ново в цикъла на събитията &quot;от други пътища&amp;quot;: това може да доведе до нежелана рекурсия! Нека да се върнем на примера с бутона. Ако извикваме QCoreApplication::processEvents() вътре в слота doWork() и потребителя натисне пак бутона, doWork() ще бъде извикан '''отново''':
<br /># main(int, char''')
# QApplication::exec&amp;amp;#40;&amp;#41;
# […]
# […]
# QWidget::event(QEvent *)
# QWidget::event(QEvent ''')<br /># Button::mousePressEvent(QMouseEvent''')  
# Button::mousePressEvent(QMouseEvent *)
# Button::clicked()  
# Button::clicked()
# […]
# […]
# Worker::doWork() // '''първо извикване'''
# Worker::doWork() // '''първо извикване'''
Line 124: Line 97:
# […]
# […]
# QWidget::event(QEvent * ) // '''друго натискане на мишката е изпратено на бутона Button…'''
# QWidget::event(QEvent * ) // '''друго натискане на мишката е изпратено на бутона Button…'''
# Button::mousePressEvent(QMouseEvent *)
# Button::mousePressEvent(QMouseEvent *)  
# Button::clicked() // '''което изпраща сигнала clicked() отново…'''
# Button::clicked() // '''което изпраща сигнала clicked() отново…'''
# […]
# […]
# Worker::doWork() // '''<span class="caps">ОПА</span>! отново се извиква нашия слот в самия себе си.'''
# Worker::doWork() // '''ОПА! отново се извиква нашия слот в самия себе си.'''


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


За щастие, това '''не''' важи за '''събитията за изтриване''' (тези публикувани в цикъла от QObject::deleteLater()). В същност, те се прихващат по специален начин от Qt, и се обработват само ако работещия цикъл на събитията има по ниско ниво на “вложеност” (по отношение на цикли на събития) от това, в което е било извикано deleteLater. За пример:
За щастие, това '''не''' важи за '''събитията за изтриване''' (тези публикувани в цикъла от QObject::deleteLater&amp;amp;#40;&amp;#41;). В същност, те се прихващат по специален начин от Qt, и се обработват само ако работещия цикъл на събитията има по ниско ниво на &quot;вложеност&amp;quot; (по отношение на цикли на събития) от това, в което е било извикано deleteLater. За пример:
 
<code><br />QObject *object = new QObject;<br />object-&gt;deleteLater();<br />QDialog dialog;<br />dialog.exec&amp;amp;#40;&amp;#41;;<br /></code>


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


.
.


=Нишкови класове в Qt=
= Нишкови класове в Qt =
 
<blockquote>
 
Компютъра е стейт машина. Нишките са за хора, които не могат да програмират стейт машини. <br /> — Алън Кокс


<blockquote>Компютъра е стейт машина. Нишките са за хора, които не могат да програмират стейт машини.<br />— Алън Кокс
</blockquote>
</blockquote>
Qt има поддръжка на нишки от много години (Qt 2.2, пуснато на 22 Sept 2000, представи класа QThread.), а от 4.0 нишките са включени по подразбиране на всички поддържани платформи (въпреки, че могат да бъдат спрени. Вижте &quot;тук&amp;quot;:http://doc.qt.nokia.com/latest/fine-tuning-features.html за повече детайли). В момента Qt предлага няколко класа за работа с нишки. Нека да започнем с общ преглед.


Qt има поддръжка на нишки от много години (Qt 2.2, пуснато на 22 Sept 2000, представи класа QThread.), а от 4.0 нишките са включени по подразбиране на всички поддържани платформи (въпреки, че могат да бъдат спрени. Вижте [http://doc.qt.nokia.com/latest/fine-tuning-features.html тук] ''[doc.qt.nokia.com]'' за повече детайли). В момента Qt предлага няколко класа за работа с нишки. Нека да започнем с общ преглед.
== QThread ==


==QThread==
&quot;QThread&amp;quot;:http://doc.qt.nokia.com/latest/qthread.html е централен клас от ниско ниво за поддръжка на нишки в Qt. Обект от тип QThread представя една нишка. Поради многоплатформеността на Qt, QThread успява да скрие платформено зависимия код, който е нужен за използването на нишки в различните операционни системи.


[http://doc.qt.nokia.com/latest/qthread.html QThread] ''[doc.qt.nokia.com]'' е централен клас от ниско ниво за поддръжка на нишки в Qt. Обект от тип QThread представя една нишка. Поради многоплатформеността на Qt, QThread успява да скрие платформено зависимия код, който е нужен за използването на нишки в различните операционни системи.
За да използваме QThread за стартиране на код в нишка, ние можем да му направим подклас и да пренапишем метода QThread::run():


За да използваме QThread за стартиране на код в нишка, ние можем да му направим подклас и да пренапишем метода QThread::run():
<code><br />class Thread : public QThread {<br />protected:<br /> void run() {<br /> /* имплементацията на вашата нишка */<br /> }<br />};<br /></code>


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


за да пуснем новата нишка. Забележете, че от Qt 4.4, QThread вече не е абстрактен клас; сега виртуалния метод QThread::run() просто извиква QThread::exec();, която пуска ''цикъла на събитията на нишката'' (повече информация за това по-късно).
<code><br />Thread '''t = new Thread;<br />t-&gt;start(); // start(), не run()!<br /></code>
<br />за да пуснем новата нишка. Забележете, че от Qt 4.4, QThread вече не е абстрактен клас; сега виртуалния метод QThread::run() просто извиква QThread::exec&amp;amp;#40;&amp;#41;;, която пуска ''цикъла на събитията на нишката'' (повече информация за това по-късно).


==QRunnable и QThreadPool==


[http://doc.qt.nokia.com/latest/qrunnable.html QRunnable] ''[doc.qt.nokia.com]'' е лек абстрактен клас, който може да се използва, за да се пусне задача в друга нишка в стила “пусни и забрави”. За да постигнете това, всичко, което трябва да направите е да създадете подклас на QRunnable и да имплементирате неговия чисто виртуален метод run():
<br />h2. QRunnable и QThreadPool
<br />&quot;QRunnable&amp;quot;:http://doc.qt.nokia.com/latest/qrunnable.html е лек абстрактен клас, който може да се използва, за да се пусне задача в друга нишка в стила &quot;пусни и забрави&amp;quot;. За да постигнете това, всичко, което трябва да направите е да създадете подклас на QRunnable и да имплементирате неговия чисто виртуален метод run():
<br /><code><br />class Task : public QRunnable {<br />public:<br /> void run() {<br /> /''' вашият код седи тук */<br /> }<br />};<br /></code>


За да пуснем QRunnable обект, ние използваме класа [http://doc.qt.nokia.com/latest/qthreadpool.html QThreadPool] ''[doc.qt.nokia.com]'' , който управлява множество от нишки. Чрез извикването на QThreadPool::start(runnable), ние добавяме QRunnable в опашката за изпълнение на QThreadPool; веднага щом нишката е на разположение, QRunnable-а ще бъде взет и пуснат на нея. Всички Qt приложения имат на разположение глобално множество от нишки, което е достъпно чрез QThreadPool::globalInstance(), но винаги могат да създадат и лична инстанция на QThreadPool и да я управляват самостоятелно.
За да пуснем QRunnable обект, ние използваме класа &quot;QThreadPool&amp;quot;:http://doc.qt.nokia.com/latest/qthreadpool.html , който управлява множество от нишки. Чрез извикването на QThreadPool::start(runnable), ние добавяме QRunnable в опашката за изпълнение на QThreadPool; веднага щом нишката е на разположение, QRunnable-а ще бъде взет и пуснат на нея. Всички Qt приложения имат на разположение глобално множество от нишки, което е достъпно чрез QThreadPool::globalInstance(), но винаги могат да създадат и лична инстанция на QThreadPool и да я управляват самостоятелно.


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


==QtConcurrent==
== QtConcurrent ==
 
&quot;QtConcurrent&amp;quot;:http://doc.qt.nokia.com/latest/threads-qtconcurrent.html е API от по-високо ниво, построено върху QThreadPool, полезно за работа с най-често срещаните паралелни компютърни шаблони: &quot;map&amp;quot;:http://en.wikipedia.org/wiki/Map_(higher-order_function), &quot;reduce&amp;quot;:http://en.wikipedia.org/wiki/Fold_(higher-order_function), и &quot;filter&amp;quot;:http://en.wikipedia.org/wiki/Filter_(higher-order_function) . Също така предоставя и метода QtConcurrent::run(), чрез който може лесно да се пусне функция в отделна нишка.
 
За разлика от QThread и QRunnable, QtConcurrent не изисква от нас да използваме синхронизация от ниско ниво - всички методи на QtConcurrent връщат &quot;QFuture&amp;quot;:http://doc.qt.nokia.com/latest/qfuture.html обект, който може да се използва за взимане на статуса на изпълнението(неговия прогрес), да прекъсва временно/стартира отново/прекратява изчислението, а също така и съдържа неговите ''резултати''. Класа &quot;QFutureWatcher&amp;quot;:http://doc.qt.nokia.com/latest/qfuturewatcher.html може да се използва за да се следи прогреса на QFuture и да се взаимодейства с него чрез сигнали и слотове (забележете, че QFuture, бидейки клас базиран на стойност, не наследява QObject).


[http://doc.qt.nokia.com/latest/threads-qtconcurrent.html QtConcurrent] ''[doc.qt.nokia.com]'' е <span class="caps">API</span> от по-високо ниво, построено върху QThreadPool, полезно за работа с най-често срещаните паралелни компютърни шаблони: [http://en.wikipedia.org/wiki/Map_(higher-order_function map] ''[en.wikipedia.org]''), [http://en.wikipedia.org/wiki/Fold_(higher-order_function reduce] ''[en.wikipedia.org]''), и [http://en.wikipedia.org/wiki/Filter_(higher-order_function filter] ''[en.wikipedia.org]'') . Също така предоставя и метода QtConcurrent::run(), чрез който може лесно да се пусне функция в отделна нишка.
== Сравнение на възможностите ==


За разлика от QThread и QRunnable, QtConcurrent не изисква от нас да използваме синхронизация от ниско ниво – всички методи на QtConcurrent връщат [http://doc.qt.nokia.com/latest/qfuture.html QFuture] ''[doc.qt.nokia.com]'' обект, който може да се използва за взимане на статуса на изпълнението(неговия прогрес), да прекъсва временно/стартира отново/прекратява изчислението, а също така и съдържа неговите ''резултати''. Класа [http://doc.qt.nokia.com/latest/qfuturewatcher.html QFutureWatcher] ''[doc.qt.nokia.com]'' може да се използва за да се следи прогреса на QFuture и да се взаимодейства с него чрез сигнали и слотове (забележете, че QFuture, бидейки клас базиран на стойност, не наследява QObject).
{|
!
!QThread
!QRunnable
!QtConcurrent<ref>С изключение на QtConcurrent::run, който е реализиран като се използва QRunnable и следователно споделя неговите преимущества и недостатъци.
</ref>
|-
|High level API
|✘
|✘
|✔
|-
|Job-oriented
|✘
|✔
|✔
|-
|Builtin support for pause/resume/cancel
|✘
|✘
|✔
|-
|Can run at a different priority
|✔
|✘
|✘
|-
|Can run an event loop
|✔
|✘
|✘
|}


==Сравнение на възможностите==
= Нишки и QObject-и =


<sup>1</sup> С изключение на QtConcurrent::run, който е реализиран като се използва QRunnable и следователно споделя неговите преимущества и недостатъци.
== Цикъл на събитията за всяка нишка ==


=Нишки и QObject-и=
До сега ние говорихме за &quot;цикълът на събитията&amp;quot;, взимайки някак си за даденост, че има само един такъв цикъл в Qt приложенията. Но това не е така: QThread обектите могат да стартират локални за нишката цикли на събитията, които да работят в нея. За това казваме, че '''главният цикъл на събитията''' е този, създаден от нишката, извикана в main() и стартирана с QCoreApplication::exec&amp;amp;#40;&amp;#41; (който ''трябва'' да бъде извикан от тази нишка). Това също така се нарича '''нишка на потребителската графика (GUI thread)''', защото е единствената нишка, в която е позволено да има операции, свързани с графичните елементи. Локалният цикъл на събитията за QThread може да се стартира като се извика QThread::exec&amp;amp;#40;&amp;#41; (вътре в нейният run() метод):


==Цикъл на събитията за всяка нишка==
<code><br />class Thread : public QThread {<br />protected:<br /> void run() {<br /> /* … инициализиране … */


До сега ние говорихме за “цикълът на събитията”, взимайки някак си за даденост, че има само един такъв цикъл в Qt приложенията. Но това не е така: QThread обектите могат да стартират локални за нишката цикли на събитията, които да работят в нея. За това казваме, че '''главният цикъл на събитията''' е този, създаден от нишката, извикана в main() и стартирана с QCoreApplication::exec() (който ''трябва'' да бъде извикан от тази нишка). Това също така се нарича '''нишка на потребителската графика (<span class="caps">GUI</span> thread)''', защото е единствената нишка, в която е позволено да има операции, свързани с графичните елементи. Локалният цикъл на събитията за QThread може да се стартира като се извика QThread::exec() (вътре в нейният run() метод):
exec&amp;amp;#40;&amp;#41;;<br /> }<br />};<br /></code>


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


Нишковият цикъл доставя събитията за всички QObject-и, които '''живеят''' в тази нишка. Това включва, по подразбиране, всички обекти, създадени в нишката или такива преместени от друга (повече информация за това по-късно). Също така казваме, че '''нишковият афинитет''' на QObject е дадена нишка, когато този обект живее в нея. Това важи за обекти, които са създадени в конструктора на QThread обект:
Нишковият цикъл доставя събитията за всички QObject-и, които '''живеят''' в тази нишка. Това включва, по подразбиране, всички обекти, създадени в нишката или такива преместени от друга (повече информация за това по-късно). Също така казваме, че '''нишковият афинитет''' на QObject е дадена нишка, когато този обект живее в нея. Това важи за обекти, които са създадени в конструктора на QThread обект:
<code><br />class MyThread : public QThread<br />{<br />public:<br /> MyThread()<br /> {<br /> otherObj = new QObject;<br /> }
private:<br /> QObject obj;<br /> QObject *otherObj;<br /> QScopedPointer&amp;lt;QObject&amp;gt; yetAnotherObj;<br />};<br /></code>


Какъв е афинитета на obj, otherObj, yetAnotherObj след като създадем обект от тип MyThread? Трябва да гледаме нишката, която ги е създала: тя е тази, която е изпълнила конструктора на MyThread. Следователно и трите обекта '''не''' живеят в нишката MyThread, а в тази, която е създала инстанцията на MyThread ( инстанцията също живее в нея).
Какъв е афинитета на obj, otherObj, yetAnotherObj след като създадем обект от тип MyThread? Трябва да гледаме нишката, която ги е създала: тя е тази, която е изпълнила конструктора на MyThread. Следователно и трите обекта '''не''' живеят в нишката MyThread, а в тази, която е създала инстанцията на MyThread ( инстанцията също живее в нея).
Line 189: Line 203:
Ние може да видим афинитета към нишка на QObject като извикаме QObject::thread(). Забележете, че QObject-ите, създадени преди QCoreApplication обекта '''нямат нишков афинитет''', и следователно за тях няма да има разпределяне на събитията (с други думи, QCoreApplication създава обекта от тип QThread, който представлява главната нишка).
Ние може да видим афинитета към нишка на QObject като извикаме QObject::thread(). Забележете, че QObject-ите, създадени преди QCoreApplication обекта '''нямат нишков афинитет''', и следователно за тях няма да има разпределяне на събитията (с други думи, QCoreApplication създава обекта от тип QThread, който представлява главната нишка).


[[Image:threadsandobjects.png]]
[[Image:http://doc.qt.nokia.com/4.7/images/threadsandobjects.png|http://doc.qt.nokia.com/4.7/images/threadsandobjects.png]]


Ние може да използваме нишково безопасният метод QCoreApplication::postEvent() за пускане на събитие за даден обект. Това ще добави събитието в опашката на цикъла на събитията за нишката, в която съществува обекта; следователно, събитието няма да се обработи, освен ако нишката няма работещ цикъл.
Ние може да използваме нишково безопасният метод QCoreApplication::postEvent() за пускане на събитие за даден обект. Това ще добави събитието в опашката на цикъла на събитията за нишката, в която съществува обекта; следователно, събитието няма да се обработи, освен ако нишката няма работещ цикъл.
Line 197: Line 211:
Освен това QWidget и всички негови подкласове, заедно с други класове, свързани с графичния интерфейс (дори не базираните на QObject като QPixmap) '''не са реентрантни''': те могат да се използват само от графичната нишка.
Освен това QWidget и всички негови подкласове, заедно с други класове, свързани с графичния интерфейс (дори не базираните на QObject като QPixmap) '''не са реентрантни''': те могат да се използват само от графичната нишка.


Можем да променим афинитета на QObject като извикаме QObject::moveToThread(). Това ще промени неговият афинитет и този на неговите деца. Тъй като QObject не е нишково безопасен, ние трябва да го използваме от нишката, в която съществува. За това, вие можете '''само''' да премествате обект '''от''' нишката, в която е, към друга нишка, но '''не и да го взимате от''' друга или да го местите между две нишки, от които никоя не е тази, в която съществува. Освен това, Qt изисква децата на QObject да са в същата нишка, в която е и той. Това означава, че:
Можем да променим афинитета на QObject като извикаме QObject::moveToThread(). Това ще промени неговият афинитет и този на неговите деца. Тъй като QObject не е нишково безопасен, ние трябва да го използваме от нишката, в която съществува. За това, вие можете '''само''' да премествате обект '''от''' нишката, в която е, към друга нишка, но '''не и да го взимате от''' друга или да го местите между две нишки, от които никоя не е тази, в която съществува. Освен това, Qt изисква децата на QObject да са в същата нишка, в която е и той. Това означава, че:<br />* не можете да използвате QObject::moveToThread() с обект, който има родител;<br />* не трябва да създавате обекти в дадена нишка, които имат за родител QThread-a създал нишката:


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


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


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


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


Статичният метод QMetaObject::invokeMethod() върши цялата работа вместо нас:
Статичният метод QMetaObject::invokeMethod() върши цялата работа вместо нас:
<code><br />QMetaObject::invokeMethod(object, &quot;methodName&amp;quot;,<br /> Qt::QueuedConnection,<br /> Q_ARG(type1, arg1),<br /> Q_ARG(type2, arg2));
<references />

Revision as of 10:51, 24 February 2015

[toc align_right="yes&quot; depth="2&quot;]

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

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

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

Тази статия е почти готова, но има нужда от още малко до изглаждане и няколко добри примера. Всякакъв вид ревюта или допълнения са добре дошли! Дискусия за статията може да намерите "тук&quot;:http://developer.qt.nokia.com/forums/viewthread/2423/ .

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

Въведение

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

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

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

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

Целта на тази статия не е да ви научи как да използвате нишки, да правите правилно заключване, да разгледа паралелизма, или да пишете скалируеми програми; има много добри книги на тези теми; на пример, вижте препоръчителния списък за четене "на тази страница&quot;:http://doc.qt.nokia.com/latest/threads.html. Вместо това, тази малка статия е предназначена да бъде пътеводител, който да въведе потребителите в нишките в Qt 4, с цел да избегнат най-честите капани и да им помогне да разработят своя код, така че той да бъде едновременно по-ясен и добре структуриран.

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

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

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

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

В тази статия ние следваме "приетите в Qt наименования&quot;:http://doc.qt.nokia.com/latest/threads-reentrancy.html , които са:

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

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

Поради това, че е Qt e система, задвижвана от събития, те и тяхното доставяне изграят централна роля в архитектурата на Qt. В тази статия няма да дадем подробно обяснение на тази тема; вместо това ние ще се фокусираме върху някои съврзани с нишките ключови концепции (вижте "тук&quot;:http://doc.qt.nokia.com/latest/eventsandfilters.html и "тук&quot;:http://doc.qt.nokia.com/qq/qq11-events.html за повече информация относно събитийната системата в Qt).

Събитие в Qt е обект, който представя нещо интересно, което се е случило; основната разлика между събитията и сигналите е, че събитията са насочени към специфичен обект в приложението (този обект решава какво да прави с това събитие), докато сигналите се излъчват "в дивото&quot;. От гледна точка на кода, всички събития са инстанции на подкласове на "QEvent&quot;:http://doc.qt.nokia.com/latest/qevent.html, и всички класове, наследяващи QObject, могат да презапишат виртуалния метод QObject::event() с цел да обработват събития, изпратени към техни инстанции.

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

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

<br />while (is_active)<br />{<br /> while (!event_queue_is_empty)<br /> dispatch_next_event();

wait_for_more_events();<br />}<br />

Ние влизаме в главният цикъл на събитията в Qt, когато извикаме QCoreApplication::exec&amp;#40;&#41;; това извикване е блокиращо и изчаква, докато не се извика 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 и изискват да има цикъл.

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

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

  1. main(int, char )
    # QApplication::exec&amp;#40;&#41;
    # […]
    # QWidget::event(QEvent
    )
    # Button::mousePressEvent(QMouseEvent)
    # Button::clicked()
    # […]
    # Worker::doWork()


В main() ние стартираме цикъла на събитията, обикновено, като извикаме QApplication::exec&amp;#40;&#41; (линия 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 обектите ще седят в опашката), няма да може да има по нататъшно взаимодействие с тях (поради същата причина), таймерите няма да могат да изпращат сигнали, че им е изтекло времето и мрежовата комуникация ще се забави и спре. Освен това, много от мениджърите на прозорци ще засекат, че вашето приложение вече не обработва събития и ще кажат на потребителя, че приложението ви не отговаря. Ето за това е толкова важно бързо да реагирате на събития и да се завръщате в цикъла на събития възможно най-бързо!
h2. Принудителна обработка на събития
Така… Какво можем да направим, ако имаме дълга задача и не искаме да блокираме цикъла на събитията? Единият възможен отговор е да сложим задачата в друга нишка( в следващите секции ще разгледаме как става това). Също така имаме възможност ръчно да накараме цикъла да се стартира като (нееднократно) извикваме CoreApplication::processEvents() вътре в нашата функция. QCoreApplication::processEvents() ще обработи събитията в опашката и след това ще продължи изпълнението на кода ви.
Друг възможен начин е чрез класа "QEventLoop&quot;:http://doc.qt.nokia.com/latest/qeventloop.html. Извиквайки QEventLoop::exec&amp;#40;&#41;, можем да влезем в цикъла на събитията и да свържем сигнали към слота QEventLoop::quit(). На пример:


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


QNetworkReply не предоставя блокиращо API и изисква цикъл за да работи. Ние влизаме в локален QEventLoop, и когато отговора е готов, напускаме цикъла.
Бъдете много внимателни, когато влизате на ново в цикъла на събитията "от други пътища&quot;: това може да доведе до нежелана рекурсия! Нека да се върнем на примера с бутона. Ако извикваме QCoreApplication::processEvents() вътре в слота doWork() и потребителя натисне пак бутона, doWork() ще бъде извикан отново:
# main(int, char)

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

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

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

<br />QObject *object = new QObject;<br />object-&gt;deleteLater();<br />QDialog dialog;<br />dialog.exec&amp;amp;#40;&amp;#41;;<br />

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

.

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

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

Qt има поддръжка на нишки от много години (Qt 2.2, пуснато на 22 Sept 2000, представи класа QThread.), а от 4.0 нишките са включени по подразбиране на всички поддържани платформи (въпреки, че могат да бъдат спрени. Вижте "тук&quot;:http://doc.qt.nokia.com/latest/fine-tuning-features.html за повече детайли). В момента Qt предлага няколко класа за работа с нишки. Нека да започнем с общ преглед.

QThread

"QThread&quot;:http://doc.qt.nokia.com/latest/qthread.html е централен клас от ниско ниво за поддръжка на нишки в Qt. Обект от тип QThread представя една нишка. Поради многоплатформеността на Qt, QThread успява да скрие платформено зависимия код, който е нужен за използването на нишки в различните операционни системи.

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

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

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

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


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



h2. QRunnable и QThreadPool
"QRunnable&quot;:http://doc.qt.nokia.com/latest/qrunnable.html е лек абстрактен клас, който може да се използва, за да се пусне задача в друга нишка в стила "пусни и забрави&quot;. За да постигнете това, всичко, което трябва да направите е да създадете подклас на QRunnable и да имплементирате неговия чисто виртуален метод run():


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

За да пуснем QRunnable обект, ние използваме класа "QThreadPool&quot;:http://doc.qt.nokia.com/latest/qthreadpool.html , който управлява множество от нишки. Чрез извикването на QThreadPool::start(runnable), ние добавяме QRunnable в опашката за изпълнение на QThreadPool; веднага щом нишката е на разположение, QRunnable-а ще бъде взет и пуснат на нея. Всички Qt приложения имат на разположение глобално множество от нишки, което е достъпно чрез QThreadPool::globalInstance(), но винаги могат да създадат и лична инстанция на QThreadPool и да я управляват самостоятелно.

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

QtConcurrent

"QtConcurrent&quot;:http://doc.qt.nokia.com/latest/threads-qtconcurrent.html е API от по-високо ниво, построено върху QThreadPool, полезно за работа с най-често срещаните паралелни компютърни шаблони: "map&quot;:http://en.wikipedia.org/wiki/Map_(higher-order_function), "reduce&quot;:http://en.wikipedia.org/wiki/Fold_(higher-order_function), и "filter&quot;:http://en.wikipedia.org/wiki/Filter_(higher-order_function) . Също така предоставя и метода QtConcurrent::run(), чрез който може лесно да се пусне функция в отделна нишка.

За разлика от QThread и QRunnable, QtConcurrent не изисква от нас да използваме синхронизация от ниско ниво - всички методи на QtConcurrent връщат "QFuture&quot;:http://doc.qt.nokia.com/latest/qfuture.html обект, който може да се използва за взимане на статуса на изпълнението(неговия прогрес), да прекъсва временно/стартира отново/прекратява изчислението, а също така и съдържа неговите резултати. Класа "QFutureWatcher&quot;:http://doc.qt.nokia.com/latest/qfuturewatcher.html може да се използва за да се следи прогреса на 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-и

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

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

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

exec&amp;amp;#40;&amp;#41;;<br /> }<br />};<br />

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

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

<br />class MyThread : public QThread<br />{<br />public:<br /> MyThread()<br /> {<br /> otherObj = new QObject;<br /> }

private:<br /> QObject obj;<br /> QObject *otherObj;<br /> QScopedPointer&amp;lt;QObject&amp;gt; yetAnotherObj;<br />};<br />

Какъв е афинитета на 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 създал нишката:

<br />class Thread : public QThread {<br /> void run() {<br /> QObject obj = new QObject(this); // ГРЕШНО[[Image:|Image:]]!<br /> }<br />};<br />

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

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

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

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

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


QMetaObject::invokeMethod(object, "methodName&quot;,
Qt::QueuedConnection,
Q_ARG(type1, arg1),
Q_ARG(type2, arg2));

  1. С изключение на QtConcurrent::run, който е реализиран като се използва QRunnable и следователно споделя неговите преимущества и недостатъци.