QT thread學習1
QRunnable Class
QRunnable類是所有可執行物件的基類。
QRunnable類是一個介面,用於表示需要執行的任務或程式碼段,由run()函式的重新實現表示。
您可以使用QThreadPool在單獨的執行緒中執行程式碼。 如果autoDelete()返回true(預設值),QThreadPool將自動刪除QRunnable。 使用setAutoDelete()來更改自動刪除標誌。
QThreadPool支援通過在run()函式中呼叫QThreadPool::tryStart(this)來多次執行同一個QRunnable。 如果autoDelete被啟用,QRunnable將在最後一個執行緒退出run函式時被刪除。 當啟用autoDelete時,多次使用同一個QRunnable呼叫QThreadPool::start()會建立一個競爭條件,不建議使用。
QThreadPool Class
QThreadPool類管理QThreads的集合。
QThreadPool管理和回收單個QThread物件,以幫助減少使用執行緒的程式中的執行緒建立成本。 每個Qt應用程式都有一個全域性QThreadPool物件,可以通過呼叫globalInstance()來訪問。
要使用其中一個QThreadPool執行緒,請繼承QRunnable並實現run()虛擬函式。 然後建立該類的一個物件,並將其傳遞給QThreadPool::start()。
class HelloWorldTask : public QRunnable {void run() { qDebug() << "Hello world from thread" << QThread::currentThread(); } }; HelloWorldTask *hello = new HelloWorldTask(); // QThreadPool takes ownership and deletes 'hello' automatically QThreadPool::globalInstance()->start(hello);
預設情況下,QThreadPool會自動刪除QRunnable。 使用QRunnable::setAutoDelete()來更改自動刪除標誌。
QThreadPool支援通過在QRunnable::run()中呼叫tryStart(this)來多次執行同一個QRunnable。 如果autoDelete被啟用,QRunnable將在最後一個執行緒退出run函式時被刪除。 當啟用autoDelete時,使用相同的QRunnable多次呼叫start()會建立一個競爭條件,不建議這樣做。
在一定時間內未使用的執行緒將過期。 預設的超時時間為30000毫秒(30秒)。 這可以使用setExpiryTimeout()來更改。 設定負過期超時將禁用過期機制。
呼叫maxThreadCount()來查詢要使用的最大執行緒數。 如果需要,您可以使用setMaxThreadCount()更改限制。 預設的maxThreadCount()是QThread::idealThreadCount()。 函式的作用是:返回當前正在工作的執行緒數。
reserveThread() 函式的作用是:為外部使用保留一個執行緒。 當你完成執行緒時,使用releaseThread(),這樣它可以被重用。 本質上,這些函式臨時增加或減少活動執行緒數,在實現QThreadPool不可見的耗時操作時非常有用。
請注意,QThreadPool是一個用於管理執行緒的低階類,請參閱Qt Concurrent模組瞭解更高級別的替代方案。
class Q_CORE_EXPORT QThreadPool : public QObject { Q_OBJECT Q_DECLARE_PRIVATE(QThreadPool) Q_PROPERTY(int expiryTimeout READ expiryTimeout WRITE setExpiryTimeout) Q_PROPERTY(int maxThreadCount READ maxThreadCount WRITE setMaxThreadCount) Q_PROPERTY(int activeThreadCount READ activeThreadCount) friend class QFutureInterfaceBase; public: QThreadPool(QObject *parent = Q_NULLPTR); ~QThreadPool(); static QThreadPool *globalInstance(); void start(QRunnable *runnable, int priority = 0); bool tryStart(QRunnable *runnable); int expiryTimeout() const; void setExpiryTimeout(int expiryTimeout); int maxThreadCount() const; void setMaxThreadCount(int maxThreadCount); int activeThreadCount() const; void reserveThread(); void releaseThread(); bool waitForDone(int msecs = -1); void clear(); Q_REQUIRED_RESULT bool tryTake(QRunnable *runnable); };
QThread Class
QThread類提供了一種平臺無關的方式來管理執行緒。
QThread物件管理程式中的一個控制執行緒。 QThreads在run()中開始執行。 預設情況下,run()通過呼叫exec()啟動事件迴圈,並在執行緒中執行Qt事件迴圈。
你可以通過使用QObject::moveToThread()將工作物件移動到執行緒中。
class Worker : public QObject { Q_OBJECT public slots: void doWork(const QString ¶meter) { QString result; /* ... here is the expensive or blocking operation ... */ emit resultReady(result); } signals: void resultReady(const QString &result); }; class Controller : public QObject { Q_OBJECT QThread workerThread; public: Controller() { Worker *worker = new Worker; worker->moveToThread(&workerThread); connect(&workerThread, &QThread::finished, worker, &QObject::deleteLater); connect(this, &Controller::operate, worker, &Worker::doWork); connect(worker, &Worker::resultReady, this, &Controller::handleResults); workerThread.start(); } ~Controller() { workerThread.quit(); workerThread.wait(); } public slots: void handleResults(const QString &); signals: void operate(const QString &); };
Worker槽中的程式碼將在一個單獨的執行緒中執行。然而,你可以自由地將Worker的插槽連線到任何執行緒中的任何物件的任何訊號。由於一種稱為佇列連線的機制,在不同的執行緒之間連線訊號和插槽是安全的。
讓程式碼在單獨的執行緒中執行的另一種方法是子類QThread並重新實現run()。例如:
class WorkerThread : public QThread { Q_OBJECT void run() override { QString result; /* ... here is the expensive or blocking operation ... */ emit resultReady(result); } signals: void resultReady(const QString &s); }; void MyObject::startWorkInAThread() { WorkerThread *workerThread = new WorkerThread(this); connect(workerThread, &WorkerThread::resultReady, this, &MyObject::handleResults); connect(workerThread, &WorkerThread::finished, workerThread, &QObject::deleteLater); workerThread->start(); }
在該示例中,執行緒將在run函式返回後退出。除非呼叫exec(),否則不會有任何事件迴圈線上程中執行 ???。
重要的是要記住,QThread例項存在於例項化它的舊執行緒中,而不是在呼叫run()的新執行緒中。這意味著QThread的所有排隊槽都將在舊執行緒中執行。因此,希望在新執行緒中呼叫插槽的開發人員必須使用工作物件方法;新的插槽不應該直接實現到子類QThread中。
當子類化QThread時,請記住,建構函式在舊執行緒中執行,而run()在新執行緒中執行。如果兩個函式都訪問一個成員變數,那麼該變數將從兩個不同的執行緒訪問。檢查這樣做是否安全。
Managing Threads
當執行緒啟動()和完成()時,QThread會通過一個訊號通知你,或者你可以使用isFinished()和isRunning()來查詢執行緒的狀態。
你可以通過呼叫exit()或quit()來停止執行緒。在極端情況下,您可能希望強制terminate()一個正在執行的執行緒。然而,這樣做是危險的,也是不被鼓勵的。有關詳細資訊,請閱讀文件中的terminate()和setTerminationEnabled()。
從Qt 4.8開始,通過將finished()訊號連線到QObject::deleteLater(),可以釋放剛剛結束的執行緒中的物件。
使用wait()來阻塞呼叫執行緒,直到另一個執行緒完成執行(或經過指定的時間)。
QThread還提供了靜態的、平臺無關的睡眠函式:sleep()、msleep()和ussleep()分別允許秒、毫秒和微秒解析度。這些函式在Qt 5.0中公開。
注意:wait()和sleep()函式通常是不必要的,因為Qt是一個事件驅動的框架。考慮偵聽finished()訊號,而不是wait()。考慮使用QTimer而不是sleep()函式。
static currentThreadId()和currentThread()返回當前執行執行緒的識別符號。前者返回執行緒的平臺特定ID;後者返回一個QThread指標。
要選擇執行緒的名稱(例如,在Linux上通過命令ps -L標識),可以在啟動執行緒之前呼叫setObjectName()。如果您沒有呼叫setObjectName(),那麼給執行緒的名稱將是執行緒物件執行時型別的類名(例如,Mandelbrot示例中的“RenderThread”,因為它是QThread子類的名稱)。請注意,這在Windows上的版本構建中目前是不可用的。
[slot] void QThread::terminate()
終止執行緒的執行。執行緒可能會立即終止,也可能不會,這取決於作業系統的排程策略。在terminate()之後使用QThread::wait()。
當執行緒終止時,所有等待該執行緒完成的執行緒將被喚醒。
警告:此函式是危險的,不鼓勵使用。執行緒可以在其程式碼路徑的任何點終止。執行緒可以在修改資料時終止。執行緒沒有機會在它自己之後進行清理,解鎖任何持有的互斥物件,等等。簡而言之,僅在絕對必要時使用此函式。
可以通過呼叫QThread::setTerminationEnabled()顯式地啟用或禁用終止。在終止被禁用時呼叫此函式將導致終止被延遲,直到重新啟用終止。
[static protected] void QThread::setTerminationEnabled(bool enabled = true)
基於enabled引數啟用或禁用當前執行緒的終止。該執行緒必須由QThread啟動。
當enabled為false時,終止被禁用。對QThread::terminate()的後續呼叫將立即返回而不生效。相反,終止被推遲到啟用終止。
當enabled為true時,表示啟用終止。以後呼叫QThread::terminate()將正常終止執行緒。如果終止被延遲(即QThread::terminate()在終止被禁用的情況下被呼叫),這個函式將立即終止呼叫執行緒。注意,在這種情況下,這個函式不會返回。
class Q_CORE_EXPORT QThread : public QObject { Q_OBJECT public: static Qt::HANDLE currentThreadId() Q_DECL_NOTHROW Q_DECL_PURE_FUNCTION; static QThread *currentThread(); static int idealThreadCount() Q_DECL_NOTHROW; static void yieldCurrentThread(); explicit QThread(QObject *parent = Q_NULLPTR); ~QThread(); enum Priority { IdlePriority, LowestPriority, LowPriority, NormalPriority, HighPriority, HighestPriority, TimeCriticalPriority, InheritPriority }; void setPriority(Priority priority); Priority priority() const; bool isFinished() const; bool isRunning() const; void requestInterruption(); bool isInterruptionRequested() const; void setStackSize(uint stackSize); uint stackSize() const; void exit(int retcode = 0); QAbstractEventDispatcher *eventDispatcher() const; void setEventDispatcher(QAbstractEventDispatcher *eventDispatcher); bool event(QEvent *event) Q_DECL_OVERRIDE; int loopLevel() const; public Q_SLOTS: void start(Priority = InheritPriority); void terminate(); void quit(); public: // default argument causes thread to block indefinetely bool wait(unsigned long time = ULONG_MAX); static void sleep(unsigned long); static void msleep(unsigned long); static void usleep(unsigned long); Q_SIGNALS: void started(QPrivateSignal); void finished(QPrivateSignal); protected: virtual void run(); int exec(); static void setTerminationEnabled(bool enabled = true); protected: QThread(QThreadPrivate &dd, QObject *parent = Q_NULLPTR); private: Q_DECLARE_PRIVATE(QThread) friend class QCoreApplication; friend class QThreadData; };
Threads and QObjects
QThread繼承QObject。它發出訊號來指示執行緒開始或結束執行,並提供一些槽位。
更有趣的是,QObjects可以在多個執行緒中使用,發出呼叫其他執行緒中的槽的訊號,並將事件傳送給其他執行緒中“活動”的物件。這是可能的,因為每個執行緒都允許有自己的事件迴圈。
QObject Reentrancy
QObject是可重入的。它的大多數非gui子類,如QTimer, QTcpSocket, QUdpSocket和QProcess,也是可重入的,這使得同時在多個執行緒中使用這些類成為可能。請注意,這些類被設計為在單個執行緒中建立和使用;在一個執行緒中建立一個物件,然後從另一個執行緒呼叫它的函式,這並不保證能夠正常工作。有三個約束條件需要注意:
- QObject的子物件必須總是在建立父物件的執行緒中建立。這意味著,除其他事項外,您不應該將QThread物件(This)作為線上程中建立的物件的父物件傳遞(因為QThread物件本身是在另一個執行緒中建立的)。
- 事件驅動物件只能在單個執行緒中使用。具體來說,這適用於定時器機制和網路模組。例如,您不能在非物件的執行緒中啟動計時器或連線套接字。
- 您必須確保在刪除QThread之前刪除執行緒中建立的所有物件。這可以通過在run()實現中在堆疊上建立物件來輕鬆完成。
雖然QObject是可重入的,但GUI類,尤其是QWidget及其所有子類,是不可重入的。它們只能在主執行緒中使用。如前所述,QCoreApplication::exec()也必須從該執行緒呼叫。
實際上,在主執行緒以外的其他執行緒中使用GUI類是不可能的,通過將耗時的操作放在單獨的工作執行緒中,並在工作執行緒完成時在主執行緒的螢幕上顯示結果,可以很容易地解決這一問題。這是用於實現Mandelbrot示例和Blocking Fortune Client示例的方法。
一般來說,在QApplication之前建立QObjects是不支援的,並且可能在退出時導致奇怪的崩潰,這取決於平臺。這意味著QObject的靜態例項也不受支援。一個合理結構的單執行緒或多執行緒應用程式應該使QApplication成為第一個建立的,最後一個銷燬的QObject。
Per-Thread Event Loop
每個執行緒都可以有自己的事件迴圈。初始執行緒使用QCoreApplication::exec()啟動它的事件迴圈,或者對於單對話方塊GUI應用程式,有時使用QDialog::exec()。其他執行緒可以使用QThread::exec()啟動事件迴圈。和QCoreApplication一樣,QThread提供了一個exit(int)函式和一個quit()槽。
執行緒中的事件迴圈使執行緒能夠使用某些非gui Qt類,這些類需要存在事件迴圈(如QTimer、QTcpSocket和QProcess)。它還可以將來自任何執行緒的訊號連線到特定執行緒的槽。這將在下面的訊號和跨執行緒槽一節中進行更詳細的解釋。
QObject例項被認為存在於建立它的執行緒中。該物件的事件由該執行緒的事件迴圈分派。QObject所在的執行緒可以使用QObject::thread()。
QObject::moveToThread()函式改變一個物件及其子物件的執行緒關聯(如果物件有父物件,則不能移動它)。
在QObject上從非擁有該物件的執行緒(或以其他方式訪問該物件)呼叫delete是不安全的,除非您保證該物件在那個時刻沒有處理事件。而使用QObject::deleteLater(),將會發佈一個DeferredDelete事件,物件執行緒的事件迴圈最終會拾取該事件。預設情況下,擁有QObject的執行緒是建立QObject的執行緒,但不是在呼叫了QObject::moveToThread()之後。
如果沒有執行事件迴圈,事件將不會被交付給物件。例如,如果你在一個執行緒中建立了一個QTimer物件,但是從來沒有呼叫exec(), QTimer將永遠不會發出它的timeout()訊號。呼叫deleteLater()也不起作用。(這些限制也適用於主執行緒。)
你可以使用執行緒安全的函式QCoreApplication::postEvent()在任何時間、任何執行緒中手動傳送事件到任何物件。事件將由建立物件的執行緒的事件迴圈自動排程。
所有執行緒都支援事件過濾器,但限制是監視物件必須與被監視物件位於同一執行緒中。類似地,QCoreApplication::sendEvent()(與postEvent()不同)只能用於將事件分派給呼叫該函式的執行緒中的物件。
Accessing QObject Subclasses from Other Threads 從其他執行緒訪問QObject子類
QObject及其所有子類都不是執行緒安全的。這包括整個事件交付系統。重要的是要記住,當您從另一個執行緒訪問物件時,事件迴圈可能會將事件傳遞給您的QObject子類。
如果你在一個QObject子類上呼叫一個不存在於當前執行緒中的函式,並且該物件可能接收到事件,你必須用互斥鎖保護所有訪問QObject子類內部資料的許可權;否則,您可能會遇到崩潰或其他不希望出現的行為。
與其他物件一樣,QThread物件位於建立物件的執行緒中——而不是在呼叫QThread::run()時建立的執行緒中。在QThread子類中提供插槽通常是不安全的,除非使用互斥保護成員變數。
另一方面,您可以安全地從QThread::run()實現發出訊號,因為訊號發出是執行緒安全的。
Signals and Slots Across Threads 執行緒間的訊號和槽
Qt支援以下訊號槽連線型別:
- Auto Connection(預設)如果訊號是在接收物件同一執行緒中發出的,那麼行為與直接連線相同。否則,行為與排隊連線相同。”
- Direct Connection 當訊號發出時,插槽立即被呼叫。槽函式在發射的執行緒中執行,這不一定是接收方的執行緒。
- Queued Connection 當控制返回到接收方執行緒的事件迴圈時,將呼叫插槽。插槽在接收方的執行緒中執行。
- Blocking QueuedConnection 插槽被呼叫作為排隊連線,除了當前執行緒塊,直到插槽返回。
注意:使用此型別連線同一執行緒中的物件將導致死鎖。
- Unique Connection 與自動連線相同,但只有在不復制現有連線的情況下才會建立連線。也就是說,如果相同的訊號已經連線到同一對物件的同一插槽,那麼連線不會建立,並且connect()返回false。
連線型別可以通過向connect()傳遞附加引數來指定。請注意,如果事件迴圈在接收方的執行緒中執行,那麼傳送方和接收方在不同的執行緒中使用直接連線是不安全的,原因與呼叫另一個執行緒中的物件上的任何函式是不安全的。
QObject::connect()本身是執行緒安全的。
Mandelbrot Example 使用QueuedConnection在工作執行緒和主執行緒之間進行通訊。為了避免凍結主線線的事件迴圈(以及應用程式的使用者介面),所有的Mandelbrot分形計算都在一個單獨的工作執行緒中完成。當完成分形渲染時,執行緒會發出訊號。
類似地,Blocking Fortune Client Example 使用一個單獨的執行緒來非同步地與TCP伺服器通訊。
Synchronizing Threads
雖然執行緒的目的是允許程式碼並行執行,但有時執行緒必須停止並等待其他執行緒。例如,如果兩個執行緒試圖同時寫入同一個變數,結果是未定義的。強制執行緒彼此等待的原理稱為互斥。這是保護共享資源(如資料)的常用技術。
Qt提供了低階原語以及高階機制來同步執行緒。
Low-Level Synchronization Primitives
QMutex是強制互斥的基本類。執行緒為了訪問共享資源而鎖定互斥物件。如果第二個執行緒在已經鎖定的情況下試圖鎖定互斥鎖,則第二個執行緒將被置於睡眠狀態,直到第一個執行緒完成任務並解鎖互斥鎖。
QReadWriteLock類似於QMutex,不同的是它區分了“讀”和“寫”訪問。當一段資料沒有被寫入時,多個執行緒同時讀取它是安全的。QMutex強制多個讀取器輪流讀取共享資料,而QReadWriteLock允許同時讀取資料,從而提高了並行性。
QSemaphore是QMutex的泛化,它保護一定數量的相同資源。相反,QMutex只保護一個資源。Semaphores Example展示了訊號量的典型應用:在生產者和消費者之間同步對迴圈緩衝區的訪問。
QWaitCondition不是通過強制互斥,而是通過提供一個條件變數來同步執行緒。其他原語使執行緒等待直到資源被解鎖,而QWaitCondition使執行緒等待直到滿足特定條件。要允許等待的執行緒繼續,呼叫wakeOne()喚醒一個隨機選擇的執行緒,或者呼叫wakeAll()同時喚醒所有執行緒。等待條件示例展示瞭如何使用QWaitCondition而不是QSemaphore來解決生產者-消費者問題。
注意:Qt的同步類依賴於正確對齊指標的使用。例如,您不能在MSVC中使用打包類。
這些同步類可用於使方法執行緒安全。然而,這樣做會導致效能損失,這就是為什麼大多數Qt方法都不是執行緒安全的。
風險
如果一個執行緒鎖定了一個資源但沒有解鎖它,應用程式可能會死鎖,因為其他執行緒將永遠無法使用該資源。例如,如果丟擲異常並強制當前函式返回而不釋放其鎖,就會發生這種情況。
另一個類似的場景是死鎖。例如,假設執行緒A正在等待執行緒B解鎖一個資源。如果執行緒B也在等待執行緒A解鎖一個不同的資源,那麼兩個執行緒都將永遠等待,因此應用程式將死鎖。
Convenience classes
QMutexLocker, QReadLocker和QWriteLocker是方便的類,使QMutex和QReadWriteLock更容易使用。它們在構造資源時鎖定資源,在銷燬資源時自動解鎖資源。它們被設計用來簡化使用QMutex和qreadwritellock的程式碼,從而減少資源被意外永久鎖定的機會。
High-Level Event Queues
Qt的事件系統對於執行緒間的通訊非常有用。每個執行緒都可以有自己的事件迴圈。要在另一個執行緒中呼叫槽(或任何可呼叫的方法),請將該呼叫放到目標執行緒的事件迴圈中。這讓目標執行緒在插槽開始執行之前完成其當前任務,而原始執行緒繼續並行執行。
要將呼叫置於事件迴圈中,請建立一個排隊的訊號槽連線。無論何時發出訊號,它的引數都會被事件系統記錄下來。訊號接收器所在的執行緒將執行這個插槽。或者,在沒有訊號的情況下呼叫QMetaObject::invokeMethod()來達到同樣的效果。在這兩種情況下,都必須使用排隊連線,因為直接連線會繞過事件系統,並立即在當前執行緒中執行該方法。
與使用低階原語不同,使用事件系統進行執行緒同步時不存在死鎖的風險。但是,事件系統並不強制互斥。如果可呼叫方法訪問共享資料,它們仍然必須使用低階原語進行保護。
話雖如此,Qt的事件系統,以及隱式共享資料結構,提供了傳統執行緒鎖定的替代方案。如果訊號和槽是獨佔使用的,並且沒有變數線上程之間共享,那麼多執行緒程式完全可以不使用低階原語。
Blocking Fortune Client Example
QTcpSocket支援兩種通用的網路程式設計方法:
- 非同步(非阻塞)方法。當控制返回到Qt的事件迴圈時,將排程並執行操作。當操作完成時,QTcpSocket發出一個訊號。例如,QTcpSocket::connectToHost()立即返回,當連線建立時,QTcpSocket發出connected()。
- 同步(阻塞)方法。在非gui和多執行緒應用程式中,你可以呼叫waitFor…()函式(例如,QTcpSocket::waitForConnected())來暫停呼叫執行緒,直到操作完成,而不是連線到訊號。
這個實現與Fortune Client的例子非常相似,但是我們沒有將QTcpSocket作為主類的成員,而是在主執行緒中執行非同步網路連線,我們將在一個單獨的執行緒中執行所有的網路操作,並使用QTcpSocket的阻塞API。
這個示例的目的是演示一個模式,您可以使用它來簡化網路程式碼,而不會失去使用者介面的響應性。使用Qt的阻塞網路API通常會導致程式碼更簡單,但由於它的阻塞行為,應該只在非gui執行緒中使用它,以防止使用者介面凍結。但與許多人的想法相反,使用QThread和執行緒並不一定會給應用程式增加難以管理的複雜性。
我們將從FortuneThread類開始,它處理網路程式碼。
class FortuneThread : public QThread
{
Q_OBJECT
public:
FortuneThread(QObject *parent = 0);
~FortuneThread();
void requestNewFortune(const QString &hostName, quint16 port);
void run() override;
signals:
void newFortune(const QString &fortune);
void error(int socketError, const QString &message);
private:
QString hostName;
quint16 port;
QMutex mutex;
QWaitCondition cond;
bool quit;
};
FortuneThread是一個QThread子類,它提供了一個用於排程算命請求的API,並且它有傳遞演算法和報告錯誤的訊號。 您可以呼叫requestNewFortune()來請求一個新的fortune,結果由newFortune()訊號傳遞。 如果出現任何錯誤,則會發出error()訊號。
需要注意的是,requestNewFortune()是從主GUI執行緒呼叫的,但它儲存的主機名和埠值將從FortuneThread的執行緒訪問。 因為我們將從不同的執行緒併發地讀寫FortuneThread的資料成員,所以我們使用QMutex來同步訪問。
void FortuneThread::requestNewFortune(const QString &hostName, quint16 port)
{
QMutexLocker locker(&mutex);
this->hostName = hostName;
this->port = port;
if (!isRunning())
start();
else
cond.wakeOne();
}
requestNewFortune()函式將fortune伺服器的主機名和埠儲存為成員資料,我們使用QMutexLocker鎖定互斥鎖來保護這些資料。 然後啟動執行緒,除非它已經在執行。 稍後我們將回到QWaitCondition::wakeOne()呼叫。
void FortuneThread::run()
{
mutex.lock();
QString serverName = hostName;
quint16 serverPort = port;
mutex.unlock();
在run()函式中,我們首先獲取互斥鎖,從成員資料中獲取主機名和埠,然後再次釋放鎖。 我們保護自己的情況是,在獲取資料的同時可以呼叫requestNewFortune()。 QString是可重入的,但不是執行緒安全的,我們還必須避免從一個請求讀取主機名和另一個請求的埠的不太可能的風險。 正如您可能已經猜到的,FortuneThread一次只能處理一個請求。
run()函式現在進入一個迴圈:
while (!quit) {
const int Timeout = 5 * 1000;
QTcpSocket socket;
socket.connectToHost(serverName, serverPort);
只要quit為false,迴圈將繼續請求fortunes。 我們通過在堆疊上建立QTcpSocket開始第一個請求,然後呼叫connectToHost()。 這將啟動一個非同步操作,在控制返回到Qt的事件迴圈後,將導致QTcpSocket發出connected()或error()。
if (!socket.waitForConnected(Timeout)) {
emit error(socket.error(), socket.errorString());
return;
}
但是由於我們執行在一個非gui執行緒中,我們不必擔心阻塞使用者介面。 因此,我們只需呼叫QTcpSocket::waitForConnected(),而不是進入一個事件迴圈。 這個函式將等待,阻塞呼叫執行緒,直到QTcpSocket發出connected()或發生錯誤。 如果觸發了connected(),則函式返回true; 如果連線失敗或超時(在本例中發生在5秒後),則返回false。 QTcpSocket::waitForConnected(),像其他的waitFor…()函式一樣,是QTcpSocket的阻塞API的一部分。
在這個語句之後,我們有一個連線的套接字要處理。
QDataStream in(&socket);
in.setVersion(QDataStream::Qt_4_0);
QString fortune;
現在我們可以建立一個QDataStream物件,將套接字傳遞給QDataStream的建構函式,並且在其他客戶機示例中,我們將流協議版本設定為QDataStream::Qt_4_0。
do {
if (!socket.waitForReadyRead(Timeout)) {
emit error(socket.error(), socket.errorString());
return;
}
in.startTransaction();
in >> fortune;
} while (!in.commitTransaction());
我們通過呼叫QTcpSocket::waitForReadyRead()初始化一個迴圈來等待fortune字串資料。 如果返回false,則中止操作。 在這個語句之後,我們開始一個流讀事務。 當QDataStream::commitTransaction()返回true時,將退出迴圈,這意味著成功載入了fortune字串。 生成的fortune通過傳送newFortune()來交付:
mutex.lock();
emit newFortune(fortune);
cond.wait(&mutex);
serverName = hostName;
serverPort = port;
mutex.unlock();
}
迴圈的最後一部分是獲取互斥量,以便安全地從成員資料中讀取資料。 然後通過呼叫QWaitCondition::wait()讓執行緒進入睡眠狀態。 在這一點上,我們可以回到requestNewFortune(),並檢視對wakeOne()的呼叫:
void FortuneThread::requestNewFortune(const QString &hostName, quint16 port)
{
...
if (!isRunning())
start();
else
cond.wakeOne();
}
這裡發生的情況是,因為執行緒在休眠狀態下等待新請求,所以我們需要在新請求到達時再次喚醒它。 QWaitCondition經常線上程中被用來發出這樣的喚醒呼叫訊號。
FortuneThread::~FortuneThread()
{
mutex.lock();
quit = true;
cond.wakeOne();
mutex.unlock();
wait();
}
結束FortuneThread,這是一個解構函式,它將quit設定為true,喚醒執行緒,並在返回之前等待執行緒退出。 這使得run()中的while迴圈將完成其當前的迭代。 當run()返回時,執行緒將終止並被銷燬。