劉 飛,張衛強,羅 彤
(寧波大學信息科學與工程學院,浙江寧波315211)
傳統的網絡服務器模型中,服務器收到一個客戶端請求后,創建一個新線程,由該線程執行任務,任務完成后,線程退出,即“即時創建,即時刪除”,對服務器來說創建線程已經比創建進程節約了不少時間,但是如果大量的客戶端對服務器過于頻繁的進行連接,該服務器就將在創建和刪除線程的過程中耗費大量時間[1]。所以減少創建和銷毀對象,尤其是減少很耗資源的對象成為提高服務程序效率的重要手段之一。線程池是一組預先創建的線程,快速、容易地處理收到的業務。比起傳統的“即時創建,即時刪除”的模型,該類型服務器節省了創建和回收線程的開銷,響應更快,效率更高[2]。
為高圖形用戶界面的響應速度,Qt提供豐富的多線程編程支持[],而為減少Qt創建和刪除線程的開銷,Qt又提供了線程池技術的支持。為降低基于Qt的網絡服務器頻繁的創建線程的開銷,本文就Qt線程池技術進行分析,研究其運行機制,并運用該技術創建一個服務器模型。
Qt提供了與平臺無關的線程類,在Qt系統中與線程池相關的最重要的類是QThreadPool和QRunnable。QThreadPool用于管理一批線程,它負責管理和回收單個線程[4]。每個 Qt應用可通過QThreadPool::globalInstance()獲得一個全局的QThreadPool對象。QRunable類表示一個任務或者一段被執行代碼的一個接口。使用QThreadPool類來運行一個QRunnable對象。把一個QRunnable放入了QThreadPool的運行隊列中,只要線程是可見的,QRunnable將會被拾起并且在那個線程里運行,QRunnable是一種輕量級的、以“run and forget”方式來在另一個線程開啟任務的抽象類,為了實現這一功能,要做的全部事情是派生QRunnable類,并實現純虛函數方法 run()[5]。
QTcpServer類不是QAbstractSocket抽象套接字類,而是繼承于QObject基類,為編寫TCP客戶端和服務器應用程序提供一個 TCP基礎服務類[6]。QTcpSocket類提供了基于TCP協議的通用接口,為監聽每一個客戶端的連接,可通過調用listen()函數來實現,每當服務器收到一個客戶端的連接請求時就會發射newConnection()信號,如果要接受待處理的連接,則可通過調用nextPendingConnection()函數,并返回一個連接的 QTcpSocket()套接字[7],該套接字作為服務端的一個子對象,可以通過使用這個返回的套接字和客戶端進行連接,這就意味著當QTcpServer對象要銷毀時,該套接字也會隨之被自動刪除,即不能在其他線程中使用該套接字,如果想在其他線程中繼續使用該套接字,那么需要重載void incomingConnection(int socketDescriptor)函數,這個函數將新創建一個QTcpSocket套接字,該函數中socketDescriptor參數是新連接的套接字描述符,然后在一個整形的待連接鏈表中將套接字存儲,最后發射信號newConnection()。
線程池就是在進程中先創建好一批線程,當有任務到來時,就從創建好的線程中取出一個線程來處理該任務,任務結束之后,將線程置為空閑,放回線程池中繼續等待下次任務的到來[8],工作過程如圖1所示。
在Qt中通過globalInstance()方法,每個 Qt的應用程序都可獲得一個全局的QThreadPool對象。

theInstance()函數功能通過Q_GLOBAL_STATIC(QThreadPool,theInstance)宏實現,以此返回一個全局的QThreadPool對象。此外由于QThreadPool類繼承QObject,在QThreadPool類中可以使用Qt提供的信號與槽機制。圖1為線程池工作原理。

圖1 線程池工作原理
通過上面介紹的函數就可以得到一個全局的QthreadPool,但是為了能夠調用該線程池中的一個線程,還需要提供繼承于QRunnable的一個類,從而實現其中的run方法。然后創建一個該類的對象,傳遞給void QThreadPool::Start(QRunnable*runnable,int priority),該函數具體實現如下所示。

該方法通過Q_D宏獲取QThreadPool命名為d的數據結構,真正開始QThreadPool一個線程是通過enqueueTask()方法實現的。


enqueueTask()方法將runnable放入隊列中來管理,并喚醒QThreadPool管理的線程池中的一個線程實現一個繼承QRunnable類的run方法。
在默認情況下,QthreadPool將能夠自動刪除創建的 QRunnable對象。使用 void QRunnable::setAutoDelete(bool autoDelete)方法可以改變這一默認行為,但是該標志必須在調用QThreadPool::Start()之前被設置,否則將會出現錯誤。
QThreadPool支持在QRunnable::run方法中通過調用tryStart(this)來多次執行相同的QRunnable。當最后一個線程退出run函數后,如果autoDelete啟用的話,將刪除QRunnable對象。在autoDelete啟用的情況下,調用start()方法多次執行同一QRunnable會產生競態,因此要避免這樣做。
通過void setExpiryTimeout(int expiryTimeout)函數來設置線程的過期時間,默認過期時間為30 s。如果設置expriyTimeout為一個負數,則代表禁止使用超時機制。如果要規定最大的線程數可通set-MaxThreadCount(int maxThreadCount)來設置,其參數maxThreadCount為要設置的數量,通過 void maxThreadCount()可以查詢可使用的最大線程數。為了確保該線程被釋放后可循環使用,可以通過函數void releaseThread()釋放該線程的,以便它可以被再次使用。
在下面的步驟中,將利用線程池技術創建一個服務器模型,以此介紹線程池的創建步驟,并通過命令客戶端對創建的服務器進行測試。
首先創建一個繼承QTcpServer的一個類,在該類的實現方法中監聽客戶端的連接每當有客戶端連接時都會調用virtual void incomingConnection(int socketDescriptor)函數[9,10],因此處理這個請求的過程就可以在這個函數中實現,對一個線程池的服務器,每當客戶端試圖連接的時候,服務器從線程池中啟動一個線程,負責對這個客戶端進行服務,所以,incomingConnection()這個函數所要做的就是建立一個線程,進而對客戶端進行服務。代碼如下,先添加類的前置聲明:

在myserver.cpp文件中,首先在構造函數中通過globalInstance()函數獲取一個全局QThreadPool對象,并設置最大線程數為20,之后實現監聽客戶端連接。


該服務器監聽到客戶端試圖建立一個套接字連接,該套接字將自動分配一個 SocketDescriptor標識,該標識會在服務器連接中使用,應當提供給每一個線程。
服務器在監聽到客戶端試圖建立socket連接時,會為此socket分配一個標識socketDescriptor,該標識在建立服務器連接時使用,所以應提供給每一個線程。接下來派生QRunnable類,并實現純虛函數run()。

在Linux環境下編譯運行服務器程序結果如圖2所示。

圖2 編譯運行
此時,如圖2所示服務器已經啟動,下面用Linux命令終端的telnet程序模擬一個客戶端,在telnet程序中輸入命令:open 127.0.0.1 1234(此處的127.0.01為程序中的設置的IP地址,即IPV4的本地主機地址,端口號是1234),請求服務器進行連接,并對其進行測試,測試結果如圖3、圖4所示。

圖3 客戶端

圖4 服務器端
通過以上步驟,利用線程池技術完成了一個服務器的創建,即使用 QThreadPool類來運行一個QRunnable對象,它維護了一個線程池。當客戶端請求連接時,服務器端調用已經創建好的線程池中的一個線程對該客戶端請求進行處理,如圖3所示,服務器將一個簡單的字符串“Hello world!!”傳遞給客戶端,并在命令客戶端顯示,此時服務器端打印出為該客戶端服務的線程ID,如圖4所示。通過以上測試,運用線程池技術實現了服務器與客戶端之間的通信。
針對目前多線程服務器在接受客戶端頻繁連接會增加開銷這一弱點,提出利用Qt線程池技術減少程序中頻繁創建線程的開銷的優勢,在服務器模型中加入線程池技術的支持,即利用QThreadPool線程池來管理一組線程,每當有客戶端連接時,就有單個線程對象來處理客戶端請求并交由該線程池管理和回收,從而減少了服務器頻繁創建線程的開銷,提高服務器工作效率。但是由于QRunnable并非QObject類,它沒有一個內置的與其他組件顯式通訊的方法,必須使用底層的線程原語(比如收集結構的枷鎖保護隊列等)來親自編寫代碼。
[1] 曾云.基于ARM+QT平臺的嵌入式賓館客服系統軟件設計[D].上海:東華大學,2011:25-31.
[2] 劉新強,曾兵義.用線程池解決服務器并發請求的方案設計[J].現代電子技術,2011,34(15):141 -143.
[3] 黃宇東,胡躍明,陳安.基于Qt的多線程技術應用于研究[J].軟件導刊,2009,8(10):40-42.
[4] 趙祖龍.基于Qt/Embedded的嵌入式跨平臺聊天系統設計[J].信息技術,2010,34(12):144 -147.
[5] 蔡志明,盧傳富,李立夏.精通Qt4編程[M].北京:電子工業出版社,2008.
[6] 崔弘珂.一種空間環境下的TCP傳輸技術研究[J].無線電通信技術,2011,37(4):21-24.
[7] 丁林松,黃麗琴.Qt4圖形設計與嵌入式開發[M].北京:人民郵電出版社,2009.
[8] 汪成林.linux環境下基于SSL的安全文件傳輸系統研究[D].杭州:浙江工業大學,2012:38-45.
[9] 馮艷紅,何加銘,楊任爾,等.基于Android藍牙技術的健康服務系統設計[J].無線電通信技術,2014,40(1):61-64.85-88.
[10]馬睿.基于Qt的TCP網絡編程研究與應用[J].福建電腦,2010,26(11):138 -139.