劉 飛,張衛(wèi)強(qiáng),羅 彤
(寧波大學(xué)信息科學(xué)與工程學(xué)院,浙江寧波315211)
傳統(tǒng)的網(wǎng)絡(luò)服務(wù)器模型中,服務(wù)器收到一個(gè)客戶(hù)端請(qǐng)求后,創(chuàng)建一個(gè)新線(xiàn)程,由該線(xiàn)程執(zhí)行任務(wù),任務(wù)完成后,線(xiàn)程退出,即“即時(shí)創(chuàng)建,即時(shí)刪除”,對(duì)服務(wù)器來(lái)說(shuō)創(chuàng)建線(xiàn)程已經(jīng)比創(chuàng)建進(jìn)程節(jié)約了不少時(shí)間,但是如果大量的客戶(hù)端對(duì)服務(wù)器過(guò)于頻繁的進(jìn)行連接,該服務(wù)器就將在創(chuàng)建和刪除線(xiàn)程的過(guò)程中耗費(fèi)大量時(shí)間[1]。所以減少創(chuàng)建和銷(xiāo)毀對(duì)象,尤其是減少很耗資源的對(duì)象成為提高服務(wù)程序效率的重要手段之一。線(xiàn)程池是一組預(yù)先創(chuàng)建的線(xiàn)程,快速、容易地處理收到的業(yè)務(wù)。比起傳統(tǒng)的“即時(shí)創(chuàng)建,即時(shí)刪除”的模型,該類(lèi)型服務(wù)器節(jié)省了創(chuàng)建和回收線(xiàn)程的開(kāi)銷(xiāo),響應(yīng)更快,效率更高[2]。
為高圖形用戶(hù)界面的響應(yīng)速度,Qt提供豐富的多線(xiàn)程編程支持[],而為減少Q(mào)t創(chuàng)建和刪除線(xiàn)程的開(kāi)銷(xiāo),Qt又提供了線(xiàn)程池技術(shù)的支持。為降低基于Qt的網(wǎng)絡(luò)服務(wù)器頻繁的創(chuàng)建線(xiàn)程的開(kāi)銷(xiāo),本文就Qt線(xiàn)程池技術(shù)進(jìn)行分析,研究其運(yùn)行機(jī)制,并運(yùn)用該技術(shù)創(chuàng)建一個(gè)服務(wù)器模型。
Qt提供了與平臺(tái)無(wú)關(guān)的線(xiàn)程類(lèi),在Qt系統(tǒng)中與線(xiàn)程池相關(guān)的最重要的類(lèi)是QThreadPool和QRunnable。QThreadPool用于管理一批線(xiàn)程,它負(fù)責(zé)管理和回收單個(gè)線(xiàn)程[4]。每個(gè) Qt應(yīng)用可通過(guò)QThreadPool::globalInstance()獲得一個(gè)全局的QThreadPool對(duì)象。QRunable類(lèi)表示一個(gè)任務(wù)或者一段被執(zhí)行代碼的一個(gè)接口。使用QThreadPool類(lèi)來(lái)運(yùn)行一個(gè)QRunnable對(duì)象。把一個(gè)QRunnable放入了QThreadPool的運(yùn)行隊(duì)列中,只要線(xiàn)程是可見(jiàn)的,QRunnable將會(huì)被拾起并且在那個(gè)線(xiàn)程里運(yùn)行,QRunnable是一種輕量級(jí)的、以“run and forget”方式來(lái)在另一個(gè)線(xiàn)程開(kāi)啟任務(wù)的抽象類(lèi),為了實(shí)現(xiàn)這一功能,要做的全部事情是派生QRunnable類(lèi),并實(shí)現(xiàn)純虛函數(shù)方法 run()[5]。
QTcpServer類(lèi)不是QAbstractSocket抽象套接字類(lèi),而是繼承于QObject基類(lèi),為編寫(xiě)TCP客戶(hù)端和服務(wù)器應(yīng)用程序提供一個(gè) TCP基礎(chǔ)服務(wù)類(lèi)[6]。QTcpSocket類(lèi)提供了基于TCP協(xié)議的通用接口,為監(jiān)聽(tīng)每一個(gè)客戶(hù)端的連接,可通過(guò)調(diào)用listen()函數(shù)來(lái)實(shí)現(xiàn),每當(dāng)服務(wù)器收到一個(gè)客戶(hù)端的連接請(qǐng)求時(shí)就會(huì)發(fā)射newConnection()信號(hào),如果要接受待處理的連接,則可通過(guò)調(diào)用nextPendingConnection()函數(shù),并返回一個(gè)連接的 QTcpSocket()套接字[7],該套接字作為服務(wù)端的一個(gè)子對(duì)象,可以通過(guò)使用這個(gè)返回的套接字和客戶(hù)端進(jìn)行連接,這就意味著當(dāng)QTcpServer對(duì)象要銷(xiāo)毀時(shí),該套接字也會(huì)隨之被自動(dòng)刪除,即不能在其他線(xiàn)程中使用該套接字,如果想在其他線(xiàn)程中繼續(xù)使用該套接字,那么需要重載void incomingConnection(int socketDescriptor)函數(shù),這個(gè)函數(shù)將新創(chuàng)建一個(gè)QTcpSocket套接字,該函數(shù)中socketDescriptor參數(shù)是新連接的套接字描述符,然后在一個(gè)整形的待連接鏈表中將套接字存儲(chǔ),最后發(fā)射信號(hào)newConnection()。
線(xiàn)程池就是在進(jìn)程中先創(chuàng)建好一批線(xiàn)程,當(dāng)有任務(wù)到來(lái)時(shí),就從創(chuàng)建好的線(xiàn)程中取出一個(gè)線(xiàn)程來(lái)處理該任務(wù),任務(wù)結(jié)束之后,將線(xiàn)程置為空閑,放回線(xiàn)程池中繼續(xù)等待下次任務(wù)的到來(lái)[8],工作過(guò)程如圖1所示。
在Qt中通過(guò)globalInstance()方法,每個(gè) Qt的應(yīng)用程序都可獲得一個(gè)全局的QThreadPool對(duì)象。
theInstance()函數(shù)功能通過(guò)Q_GLOBAL_STATIC(QThreadPool,theInstance)宏實(shí)現(xiàn),以此返回一個(gè)全局的QThreadPool對(duì)象。此外由于QThreadPool類(lèi)繼承QObject,在QThreadPool類(lèi)中可以使用Qt提供的信號(hào)與槽機(jī)制。圖1為線(xiàn)程池工作原理。
圖1 線(xiàn)程池工作原理
通過(guò)上面介紹的函數(shù)就可以得到一個(gè)全局的QthreadPool,但是為了能夠調(diào)用該線(xiàn)程池中的一個(gè)線(xiàn)程,還需要提供繼承于QRunnable的一個(gè)類(lèi),從而實(shí)現(xiàn)其中的run方法。然后創(chuàng)建一個(gè)該類(lèi)的對(duì)象,傳遞給void QThreadPool::Start(QRunnable*runnable,int priority),該函數(shù)具體實(shí)現(xiàn)如下所示。
該方法通過(guò)Q_D宏獲取QThreadPool命名為d的數(shù)據(jù)結(jié)構(gòu),真正開(kāi)始QThreadPool一個(gè)線(xiàn)程是通過(guò)enqueueTask()方法實(shí)現(xiàn)的。
enqueueTask()方法將runnable放入隊(duì)列中來(lái)管理,并喚醒QThreadPool管理的線(xiàn)程池中的一個(gè)線(xiàn)程實(shí)現(xiàn)一個(gè)繼承QRunnable類(lèi)的run方法。
在默認(rèn)情況下,QthreadPool將能夠自動(dòng)刪除創(chuàng)建的 QRunnable對(duì)象。使用 void QRunnable::setAutoDelete(bool autoDelete)方法可以改變這一默認(rèn)行為,但是該標(biāo)志必須在調(diào)用QThreadPool::Start()之前被設(shè)置,否則將會(huì)出現(xiàn)錯(cuò)誤。
QThreadPool支持在QRunnable::run方法中通過(guò)調(diào)用tryStart(this)來(lái)多次執(zhí)行相同的QRunnable。當(dāng)最后一個(gè)線(xiàn)程退出run函數(shù)后,如果autoDelete啟用的話(huà),將刪除QRunnable對(duì)象。在autoDelete啟用的情況下,調(diào)用start()方法多次執(zhí)行同一QRunnable會(huì)產(chǎn)生競(jìng)態(tài),因此要避免這樣做。
通過(guò)void setExpiryTimeout(int expiryTimeout)函數(shù)來(lái)設(shè)置線(xiàn)程的過(guò)期時(shí)間,默認(rèn)過(guò)期時(shí)間為30 s。如果設(shè)置expriyTimeout為一個(gè)負(fù)數(shù),則代表禁止使用超時(shí)機(jī)制。如果要規(guī)定最大的線(xiàn)程數(shù)可通set-MaxThreadCount(int maxThreadCount)來(lái)設(shè)置,其參數(shù)maxThreadCount為要設(shè)置的數(shù)量,通過(guò) void maxThreadCount()可以查詢(xún)可使用的最大線(xiàn)程數(shù)。為了確保該線(xiàn)程被釋放后可循環(huán)使用,可以通過(guò)函數(shù)void releaseThread()釋放該線(xiàn)程的,以便它可以被再次使用。
在下面的步驟中,將利用線(xiàn)程池技術(shù)創(chuàng)建一個(gè)服務(wù)器模型,以此介紹線(xiàn)程池的創(chuàng)建步驟,并通過(guò)命令客戶(hù)端對(duì)創(chuàng)建的服務(wù)器進(jìn)行測(cè)試。
首先創(chuàng)建一個(gè)繼承QTcpServer的一個(gè)類(lèi),在該類(lèi)的實(shí)現(xiàn)方法中監(jiān)聽(tīng)客戶(hù)端的連接每當(dāng)有客戶(hù)端連接時(shí)都會(huì)調(diào)用virtual void incomingConnection(int socketDescriptor)函數(shù)[9,10],因此處理這個(gè)請(qǐng)求的過(guò)程就可以在這個(gè)函數(shù)中實(shí)現(xiàn),對(duì)一個(gè)線(xiàn)程池的服務(wù)器,每當(dāng)客戶(hù)端試圖連接的時(shí)候,服務(wù)器從線(xiàn)程池中啟動(dòng)一個(gè)線(xiàn)程,負(fù)責(zé)對(duì)這個(gè)客戶(hù)端進(jìn)行服務(wù),所以,incomingConnection()這個(gè)函數(shù)所要做的就是建立一個(gè)線(xiàn)程,進(jìn)而對(duì)客戶(hù)端進(jìn)行服務(wù)。代碼如下,先添加類(lèi)的前置聲明:
在myserver.cpp文件中,首先在構(gòu)造函數(shù)中通過(guò)globalInstance()函數(shù)獲取一個(gè)全局QThreadPool對(duì)象,并設(shè)置最大線(xiàn)程數(shù)為20,之后實(shí)現(xiàn)監(jiān)聽(tīng)客戶(hù)端連接。
該服務(wù)器監(jiān)聽(tīng)到客戶(hù)端試圖建立一個(gè)套接字連接,該套接字將自動(dòng)分配一個(gè) SocketDescriptor標(biāo)識(shí),該標(biāo)識(shí)會(huì)在服務(wù)器連接中使用,應(yīng)當(dāng)提供給每一個(gè)線(xiàn)程。
服務(wù)器在監(jiān)聽(tīng)到客戶(hù)端試圖建立socket連接時(shí),會(huì)為此socket分配一個(gè)標(biāo)識(shí)socketDescriptor,該標(biāo)識(shí)在建立服務(wù)器連接時(shí)使用,所以應(yīng)提供給每一個(gè)線(xiàn)程。接下來(lái)派生QRunnable類(lèi),并實(shí)現(xiàn)純虛函數(shù)run()。
在Linux環(huán)境下編譯運(yùn)行服務(wù)器程序結(jié)果如圖2所示。
圖2 編譯運(yùn)行
此時(shí),如圖2所示服務(wù)器已經(jīng)啟動(dòng),下面用Linux命令終端的telnet程序模擬一個(gè)客戶(hù)端,在telnet程序中輸入命令:open 127.0.0.1 1234(此處的127.0.01為程序中的設(shè)置的IP地址,即IPV4的本地主機(jī)地址,端口號(hào)是1234),請(qǐng)求服務(wù)器進(jìn)行連接,并對(duì)其進(jìn)行測(cè)試,測(cè)試結(jié)果如圖3、圖4所示。
圖3 客戶(hù)端
圖4 服務(wù)器端
通過(guò)以上步驟,利用線(xiàn)程池技術(shù)完成了一個(gè)服務(wù)器的創(chuàng)建,即使用 QThreadPool類(lèi)來(lái)運(yùn)行一個(gè)QRunnable對(duì)象,它維護(hù)了一個(gè)線(xiàn)程池。當(dāng)客戶(hù)端請(qǐng)求連接時(shí),服務(wù)器端調(diào)用已經(jīng)創(chuàng)建好的線(xiàn)程池中的一個(gè)線(xiàn)程對(duì)該客戶(hù)端請(qǐng)求進(jìn)行處理,如圖3所示,服務(wù)器將一個(gè)簡(jiǎn)單的字符串“Hello world!!”傳遞給客戶(hù)端,并在命令客戶(hù)端顯示,此時(shí)服務(wù)器端打印出為該客戶(hù)端服務(wù)的線(xiàn)程ID,如圖4所示。通過(guò)以上測(cè)試,運(yùn)用線(xiàn)程池技術(shù)實(shí)現(xiàn)了服務(wù)器與客戶(hù)端之間的通信。
針對(duì)目前多線(xiàn)程服務(wù)器在接受客戶(hù)端頻繁連接會(huì)增加開(kāi)銷(xiāo)這一弱點(diǎn),提出利用Qt線(xiàn)程池技術(shù)減少程序中頻繁創(chuàng)建線(xiàn)程的開(kāi)銷(xiāo)的優(yōu)勢(shì),在服務(wù)器模型中加入線(xiàn)程池技術(shù)的支持,即利用QThreadPool線(xiàn)程池來(lái)管理一組線(xiàn)程,每當(dāng)有客戶(hù)端連接時(shí),就有單個(gè)線(xiàn)程對(duì)象來(lái)處理客戶(hù)端請(qǐng)求并交由該線(xiàn)程池管理和回收,從而減少了服務(wù)器頻繁創(chuàng)建線(xiàn)程的開(kāi)銷(xiāo),提高服務(wù)器工作效率。但是由于QRunnable并非QObject類(lèi),它沒(méi)有一個(gè)內(nèi)置的與其他組件顯式通訊的方法,必須使用底層的線(xiàn)程原語(yǔ)(比如收集結(jié)構(gòu)的枷鎖保護(hù)隊(duì)列等)來(lái)親自編寫(xiě)代碼。
[1] 曾云.基于ARM+QT平臺(tái)的嵌入式賓館客服系統(tǒng)軟件設(shè)計(jì)[D].上海:東華大學(xué),2011:25-31.
[2] 劉新強(qiáng),曾兵義.用線(xiàn)程池解決服務(wù)器并發(fā)請(qǐng)求的方案設(shè)計(jì)[J].現(xiàn)代電子技術(shù),2011,34(15):141 -143.
[3] 黃宇東,胡躍明,陳安.基于Qt的多線(xiàn)程技術(shù)應(yīng)用于研究[J].軟件導(dǎo)刊,2009,8(10):40-42.
[4] 趙祖龍.基于Qt/Embedded的嵌入式跨平臺(tái)聊天系統(tǒng)設(shè)計(jì)[J].信息技術(shù),2010,34(12):144 -147.
[5] 蔡志明,盧傳富,李立夏.精通Qt4編程[M].北京:電子工業(yè)出版社,2008.
[6] 崔弘珂.一種空間環(huán)境下的TCP傳輸技術(shù)研究[J].無(wú)線(xiàn)電通信技術(shù),2011,37(4):21-24.
[7] 丁林松,黃麗琴.Qt4圖形設(shè)計(jì)與嵌入式開(kāi)發(fā)[M].北京:人民郵電出版社,2009.
[8] 汪成林.linux環(huán)境下基于SSL的安全文件傳輸系統(tǒng)研究[D].杭州:浙江工業(yè)大學(xué),2012:38-45.
[9] 馮艷紅,何加銘,楊任爾,等.基于Android藍(lán)牙技術(shù)的健康服務(wù)系統(tǒng)設(shè)計(jì)[J].無(wú)線(xiàn)電通信技術(shù),2014,40(1):61-64.85-88.
[10]馬睿.基于Qt的TCP網(wǎng)絡(luò)編程研究與應(yīng)用[J].福建電腦,2010,26(11):138 -139.