<ruby id="bdb3f"></ruby>

    <p id="bdb3f"><cite id="bdb3f"></cite></p>

      <p id="bdb3f"><cite id="bdb3f"><th id="bdb3f"></th></cite></p><p id="bdb3f"></p>
        <p id="bdb3f"><cite id="bdb3f"></cite></p>

          <pre id="bdb3f"></pre>
          <pre id="bdb3f"><del id="bdb3f"><thead id="bdb3f"></thead></del></pre>

          <ruby id="bdb3f"><mark id="bdb3f"></mark></ruby><ruby id="bdb3f"></ruby>
          <pre id="bdb3f"><pre id="bdb3f"><mark id="bdb3f"></mark></pre></pre><output id="bdb3f"></output><p id="bdb3f"></p><p id="bdb3f"></p>

          <pre id="bdb3f"><del id="bdb3f"><progress id="bdb3f"></progress></del></pre>

                <ruby id="bdb3f"></ruby>

                ??碼云GVP開源項目 12k star Uniapp+ElementUI 功能強大 支持多語言、二開方便! 廣告
                #(72):線程和事件循環 前面一章我們簡單介紹了如何使用`QThread`實現線程。現在我們開始詳細介紹如何“正確”編寫多線程程序。我們這里的大部分內容來自于[Qt的一篇Wiki文檔](http://qt-project.org/wiki/Threads_Events_QObjects),有興趣的童鞋可以去看原文。 在介紹在以前,我們要認識兩個術語: * **可重入的(Reentrant)**:如果多個線程可以在同一時刻調用一個類的所有函數,并且保證每一次函數調用都引用一個唯一的數據,就稱這個類是可重入的(Reentrant means that all the functions in the referenced class can be called simultaneously by multiple threads, provided that each invocation of the functions reference unique data.)。大多數 C++ 類都是可重入的。類似的,一個函數被稱為可重入的,如果該函數允許多個線程在同一時刻調用,而每一次的調用都只能使用其獨有的數據。全局變量就不是函數獨有的數據,而是共享的。換句話說,這意味著類或者函數的使用者必須使用某種額外的機制(比如鎖)來控制對對象的實例或共享數據的序列化訪問。 * **線程安全(Thread-safe)**:如果多個線程可以在同一時刻調用一個類的所有函數,即使每一次函數調用都引用一個共享的數據,就說這個類是線程安全的(Threadsafe means that all the functions in the referenced class can be called simultaneously by multiple threads even when each invocation references shared data.)。如果多個線程可以在同一時刻訪問函數的共享數據,就稱這個函數是線程安全的。 進一步說,對于一個類,如果不同的實例可以被不同線程同時使用而不受影響,就說這個類是可重入的;如果這個類的所有成員函數都可以被不同線程同時調用而不受影響,即使這些調用針對同一個對象,那么我們就說這個類是線程安全的。由此可以看出,線程安全的語義要強于可重入。接下來,我們從事件開始討論。之前我們說過,Qt 是事件驅動的。在 Qt 中,事件由一個普通對象表示(`QEvent`或其子類)。這是事件與信號的一個很大區別:事件總是由某一種類型的對象表示,針對某一個特殊的對象,而信號則沒有這種目標對象。所有`QObject`的子類都可以通過覆蓋`QObject::event()`函數來控制事件的對象。 事件可以由程序生成,也可以在程序外部生成。例如: * `QKeyEvent`和`QMouseEvent`對象表示鍵盤或鼠標的交互,通常由系統的窗口管理器產生; * `QTimerEvent`事件在定時器超時時發送給一個`QObject`,定時器事件通常由操作系統發出; * `QChildEvent`在增加或刪除子對象時發送給一個`QObject`,這是由 Qt 應用程序自己發出的。 需要注意的是,與信號不同,事件并不是一產生就被分發。事件產生之后被加入到一個隊列中(這里的隊列含義同數據結構中的概念,先進先出),該隊列即被稱為事件隊列。事件分發器遍歷事件隊列,如果發現事件隊列中有事件,那么就把這個事件發送給它的目標對象。這個循環被稱作事件循環。事件循環的偽代碼描述大致如下所示: ~~~ while (is_active) { while (!event_queue_is_empty) { dispatch_next_event(); } wait_for_more_events(); } ~~~ 正如前面所說的,調用`QCoreApplication::exec()`?函數意味著進入了主循環。我們把事件循環理解為一個無限循環,直到`QCoreApplication::exit()`或者`QCoreApplication::quit()`被調用,事件循環才真正退出。 偽代碼里面的`while`會遍歷整個事件隊列,發送從隊列中找到的事件;`wait_for_more_events()`函數則會阻塞事件循環,直到又有新的事件產生。我們仔細考慮這段代碼,在`wait_for_more_events()`函數所得到的新的事件都應該是由程序外部產生的。因為所有內部事件都應該在事件隊列中處理完畢了。因此,我們說事件循環在`wait_for_more_events()`函數進入休眠,并且可以被下面幾種情況喚醒: * 窗口管理器的動作(鍵盤、鼠標按鍵按下、與窗口交互等); * 套接字動作(網絡傳來可讀的數據,或者是套接字非阻塞寫等); * 定時器; * 由其它線程發出的事件(我們會在后文詳細解釋這種情況)。 在類 UNIX 系統中,窗口管理器(比如 X11)會通過套接字(Unix Domain 或 TCP/IP)向應用程序發出窗口活動的通知,因為客戶端就是通過這種機制與 X 服務器交互的。如果我們決定要實現基于內部的`socketpair(2)`函數的跨線程事件的派發,那么窗口的管理活動需要喚醒的是: * 套接字 socket * 定時器 timer 這也正是`select(2)`系統調用所做的:它監視窗口活動的一組描述符,如果在一定時間內沒有活動,它會發出超時消息(這種超時是可配置的)。Qt 所要做的,就是把`select()`的返回值轉換成一個合適的`QEvent`子類的對象,然后將其放入事件隊列。好了,現在你已經知道事件循環的內部機制了。 至于為什么需要事件循環,我們可以簡單列出一個清單: * **組件的繪制與交互**:`QWidget::paintEvent()`會在發出`QPaintEvent`事件時被調用。該事件可以通過內部`QWidget::update()`調用或者窗口管理器(例如顯示一個隱藏的窗口)發出。所有交互事件(鍵盤、鼠標)也是類似的:這些事件都要求有一個事件循環才能發出。 * **定時器**:長話短說,它們會在`select(2)`或其他類似的調用超時時被發出,因此你需要允許 Qt 通過返回事件循環來實現這些調用。 * **網絡**:所有低級網絡類(`QTcpSocket`、`QUdpSocket`以及`QTcpServer`等)都是異步的。當你調用`read()`函數時,它們僅僅返回已可用的數據;當你調用`write()`函數時,它們僅僅將寫入列入計劃列表稍后執行。只有返回事件循環的時候,真正的讀寫才會執行。注意,這些類也有同步函數(以`waitFor`開頭的函數),但是它們并不推薦使用,就是因為它們會阻塞事件循環。高級的類,例如`QNetworkAccessManager`則根本不提供同步 API,因此必須要求事件循環。 有了事件循環,你就會想怎樣阻塞它。阻塞它的理由可能有很多,例如我就想讓`QNetworkAccessManager`同步執行。在解釋為什么**永遠不要阻塞事件循環**之前,我們要了解究竟什么是“阻塞”。假設我們有一個按鈕`Button`,這個按鈕在點擊時會發出一個信號。這個信號會與一個`Worker`對象連接,這個`Worker`對象會執行很耗時的操作。當點擊了按鈕之后,我們觀察從上到下的函數調用堆棧: ~~~ main(int, char **) QApplication::exec() […] QWidget::event(QEvent *) Button::mousePressEvent(QMouseEvent *) Button::clicked() […] Worker::doWork() ~~~ 我們在`main()`函數開始事件循環,也就是常見的`QApplication::exec()`函數。窗口管理器偵測到鼠標點擊后,Qt 會發現并將其轉換成`QMouseEvent`事件,發送給組件的`event()`函數。這一過程是通過`QApplication::notify()`函數實現的。注意我們的按鈕并沒有覆蓋`event()`函數,因此其父類的實現將被執行,也就是`QWidget::event()`函數。這個函數發現這個事件是一個鼠標點擊事件,于是調用了對應的事件處理函數,就是`Button::mousePressEvent()`函數。我們重寫了這個函數,發出`Button::clicked()`信號,而正是這個信號會調用`Worker::doWork()`槽函數。有關這一機制我們在前面的事件部分曾有闡述,如果不明白這部分機制,請參考[前面的章節](http://www.devbean.net/2012/10/qt-study-road-2-event-func/)。 在`worker`努力工作的時候,事件循環在干什么?或許你已經猜到了答案:什么都沒做!事件循環發出了鼠標按下的事件,然后等著事件處理函數返回。此時,它一直是阻塞的,直到`Worker::doWork()`函數結束。注意,我們使用了“阻塞”一詞,也就是說,所謂**阻塞事件循環**,意思是沒有事件被派發處理。 在事件就此卡住時,**組件也不會更新自身**(因為`QPaintEvent`對象還在隊列中),**也不會有其它什么交互發生**(還是同樣的原因),**定時器也不會超時**并且**網絡交互會越來越慢直到停止**。也就是說,前面我們大費周折分析的各種依賴事件循環的活動都會停止。這時候,需要窗口管理器會檢測到你的應用程序不再處理任何事件,于是**告訴用戶你的程序失去響應**。這就是為什么我們需要快速地處理事件,并且盡可能快地返回事件循環。 現在,重點來了:我們不可能避免業務邏輯中的耗時操作,那么怎樣做才能既可以執行那些耗時的操作,又不會阻塞事件循環呢?一般會有三種解決方案:第一,我們將任務移到另外的線程(正如我們[上一章](http://www.devbean.net/2013/11/qt-study-road-2-thread-intro/)看到的那樣,不過現在我們暫時略過這部分內容);第二,我們手動強制運行事件循環。想要強制運行事件循環,我們需要在耗時的任務中一遍遍地調用`QCoreApplication::processEvents()`函數。`QCoreApplication::processEvents()`函數會發出事件隊列中的所有事件,并且立即返回到調用者。仔細想一下,我們在這里所做的,就是模擬了一個事件循環。 另外一種解決方案我們在[前面的章節](http://www.devbean.net/2013/11/qt-study-road-2-access-network-4/)提到過:使用`QEventLoop`類重新進入新的事件循環。通過調用`QEventLoop::exec()`函數,我們重新進入新的事件循環,給`QEventLoop::quit()`槽函數發送信號則退出這個事件循環。拿前面的例子來說: ~~~ QEventLoop eventLoop; connect(netWorker, &NetWorker::finished, &eventLoop, &QEventLoop::quit); QNetworkReply *reply = netWorker->get(url); replyMap.insert(reply, FetchWeatherInfo); eventLoop.exec(); ~~~ `QNetworkReply`沒有提供阻塞式 API,并且要求有一個事件循環。我們通過一個局部的`QEventLoop`來達到這一目的:當網絡響應完成時,這個局部的事件循環也會退出。 前面我們也強調過:通過“其它的入口”進入事件循環要特別小心:因為它會導致遞歸調用!現在我們可以看看為什么會導致遞歸調用了。回過頭來看看按鈕的例子。當我們在`Worker::doWork()`槽函數中調用了`QCoreApplication::processEvents()`函數時,用戶再次點擊按鈕,槽函數`Worker::doWork()又`**一次**被調用: ~~~ main(int, char **) QApplication::exec() […] QWidget::event(QEvent *) Button::mousePressEvent(QMouseEvent *) Button::clicked() […] Worker::doWork() // <strong>第一次調用</strong> QCoreApplication::processEvents() // <strong>手動發出所有事件</strong> […] QWidget::event(QEvent * ) // <strong>用戶又點擊了一下按鈕…</strong> Button::mousePressEvent(QMouseEvent *) Button::clicked() // <strong>又發出了信號…</strong> […] Worker::doWork() // <strong>遞歸進入了槽函數!</strong> ~~~ 當然,這種情況也有解決的辦法:我們可以在調用`QCoreApplication::processEvents()`函數時傳入`QEventLoop::ExcludeUserInputEvents`參數,意思是不要再次派發用戶輸入事件(這些事件仍舊會保留在事件隊列中)。 幸運的是,在**刪除事件**(也就是由`QObject::deleteLater()`函數加入到事件隊列中的事件)中,**沒有**這個問題。這是因為刪除事件是由另外的機制處理的。刪除事件只有在事件循環有比較小的“嵌套”的情況下才會被處理,而不是調用了`deleteLater()`函數的那個循環。例如: ~~~ QObject *object = new QObject; object->deleteLater(); QDialog dialog; dialog.exec(); ~~~ 這段代碼**并不會**造成野指針(注意,`QDialog::exec()`的調用是嵌套在`deleteLater()`調用所在的事件循環之內的)。通過`QEventLoop`進入局部事件循環也是類似的。在 Qt 4.7.3 中,唯一的例外是,在沒有事件循環的情況下直接調用`deleteLater()`函數,那么,之后第一個進入的事件循環會獲取這個事件,然后直接將這個對象刪除。不過這也是合理的,因為 Qt 本來不知道會執行刪除操作的那個“外部的”事件循環,所以第一個事件循環就會直接刪除對象。
                  <ruby id="bdb3f"></ruby>

                  <p id="bdb3f"><cite id="bdb3f"></cite></p>

                    <p id="bdb3f"><cite id="bdb3f"><th id="bdb3f"></th></cite></p><p id="bdb3f"></p>
                      <p id="bdb3f"><cite id="bdb3f"></cite></p>

                        <pre id="bdb3f"></pre>
                        <pre id="bdb3f"><del id="bdb3f"><thead id="bdb3f"></thead></del></pre>

                        <ruby id="bdb3f"><mark id="bdb3f"></mark></ruby><ruby id="bdb3f"></ruby>
                        <pre id="bdb3f"><pre id="bdb3f"><mark id="bdb3f"></mark></pre></pre><output id="bdb3f"></output><p id="bdb3f"></p><p id="bdb3f"></p>

                        <pre id="bdb3f"><del id="bdb3f"><progress id="bdb3f"></progress></del></pre>

                              <ruby id="bdb3f"></ruby>

                              哎呀哎呀视频在线观看