多線(xiàn)程確實(shí)好用,但也有它的不足之處,當我們最初開(kāi)始使用多線(xiàn)程時(shí),它似乎是程序員的圣杯!誰(shuí)不希望有幾百個(gè)線(xiàn)程同時(shí)運行呢?誰(shuí)不希望程序在性能上能夠無(wú)縫地升級到二處理器、四處理器、甚至八處理器的服務(wù)器上呢?
接下來(lái)就開(kāi)始試著(zhù)使用線(xiàn)程。創(chuàng )建一些測試程序并運行它們,然后當一半線(xiàn)程毫無(wú)緣由地停止工作時(shí)我們對著(zhù)顯示器猛擊鍵盤(pán)。是的,線(xiàn)程是造成這種情況的罪魁禍首。以下是線(xiàn)程挫傷人們積極性的幾種方法。
如果了解現代計算機的工作原理,就會(huì )明白每個(gè)程序都有一個(gè)數據結構,這一數據結構稱(chēng)為“堆棧(stack)”,它了解正在執行的每個(gè)函數的本地數據。如果對堆棧的了解還不太多,也沒(méi)關(guān)系,只需要知道程序有一個(gè)堆棧即可。程序堆棧通常需要相當數量的內存,這取決于有多少函數被調用。有些系統使用可變大小的堆棧,而有些系統則使用固定大小的堆棧。無(wú)論采用哪種方法,操作系統都力圖要完全確保堆棧的大小足夠大,以便在同時(shí)調用許多層函數時(shí)可以防止溢出。
注意:每個(gè)線(xiàn)程也了解其他一些事情,例如CPU的精確狀態(tài)。但是,與其他所有事情相比,到目前為止,堆棧是線(xiàn)程最大的開(kāi)銷(xiāo)。
當使用多線(xiàn)程,而許多線(xiàn)程本身的運作類(lèi)似于一個(gè)小程序時(shí),會(huì )突然意識到每個(gè)線(xiàn)程都需要它自己的堆棧。如果計劃對整個(gè)程序中的每個(gè)小任務(wù)都創(chuàng )建一個(gè)線(xiàn)程,則可能妨礙很大,因為即使是最小的線(xiàn)程也需要它自己的堆棧。
堆棧的大小范圍可以小到幾千字節,大到幾兆字節,這與實(shí)現有關(guān)。因此在瘋狂地創(chuàng )建幾千個(gè)線(xiàn)程之前,我們需要牢記這一點(diǎn)。
在一個(gè)單處理器系統中,線(xiàn)程需要額外的處理開(kāi)銷(xiāo),因為操作系統通過(guò)計算來(lái)確定什么時(shí)候應該運行哪個(gè)線(xiàn)程以及每個(gè)線(xiàn)程應該運行多長(cháng)時(shí)間。如果有幾千個(gè)線(xiàn)程,這就需要非常強的處理能力,從長(cháng)遠來(lái)看有可能減慢程序的執行速度。但是,對于使用阻塞IO調用的系統來(lái)說(shuō),線(xiàn)程處理是絕對必要的,實(shí)在沒(méi)有什么方法可以繞開(kāi)它。
但是,非常令人感到欣慰的是,多線(xiàn)程處理的了不起之處在于它的理論性。如果在一臺兩處理器機器上運行程序,則省去了大部分(而非全部)額外的處理開(kāi)銷(xiāo),程序運行速度也比在單處理器機器上快。
警告:人們常常錯誤地認為,如果在一臺兩處理器機器上運行一個(gè)程序,則它的運行速度應該是單處理器機器上的兩倍。但是,實(shí)際情況確實(shí)不是如此。四處理器機器和八處理器機器的情況也與此相同。這是由“收益遞減定律(lawof diminishingreturns)”引起的。在機器上每增加一個(gè)新的處理器,則必然也增大了額外開(kāi)銷(xiāo)。到目前為止,n-處理器機器面臨的最大問(wèn)題是內存帶寬(memorybandwidth)。在給定的時(shí)間內,只有一定量的內存能夠從主存傳送到每個(gè)處理器,因此,當開(kāi)始增加處理器時(shí),它們常常需要比內存總線(xiàn)能夠處理的內存更多的內存。在這樣的情況下,最終結果是系統中的大多數處理器在大部分時(shí)間內都在等待從內存中讀取信息或者將信息寫(xiě)入到內存中,而不是真正地執行處理任務(wù)。
死鎖是一個(gè)令人頭疼的問(wèn)題,僅僅只是提到它就足以使大多數程序員聞風(fēng)而逃。如果不對線(xiàn)程與其他線(xiàn)程之間交互作用的每一種方法進(jìn)行仔細考慮,相信大家都會(huì )遇到死鎖 問(wèn)題。
那么什么是死鎖呢?假定有兩個(gè)資源A和B,有兩個(gè)線(xiàn)程1和2。線(xiàn)程1使用互斥鎖將資源A鎖住,并使用該資源。與此同時(shí),由于線(xiàn)程2需要使用資源B,于是線(xiàn)程2將資源B鎖住。片刻之后,線(xiàn)程1需要使用資源B,因此它也試圖要鎖住資源B。因為互斥鎖在等待資源可以使用,所以線(xiàn)程1開(kāi)始等待。接著(zhù),過(guò)了一會(huì )兒,線(xiàn)程2需要使用資源A,因此它也試圖要鎖住資源A,并且進(jìn)入互斥鎖循環(huán)等待。
這會(huì )發(fā)生什么情況呢?線(xiàn)程1正在等待的資源B為線(xiàn)程2所擁有,但是由于線(xiàn)程2正在等待的資源A為線(xiàn)程1所擁有,所以線(xiàn)程2并不會(huì )解除資源B的鎖定。于是,兩個(gè)線(xiàn)程都在等待另一個(gè)線(xiàn)程釋放其資源,這就是死鎖。
圖3.7展示了這一過(guò)程,它是兩個(gè)線(xiàn)程試圖獲取相同的資源而導致死鎖的簡(jiǎn)單案例。
有許多情況可以產(chǎn)生死鎖,但死鎖不一定會(huì )立即顯現出來(lái)。這是開(kāi)發(fā)多線(xiàn)程程序所面臨的最困難的問(wèn)題之一。

圖3.7 試圖獲取相同資源的兩個(gè)線(xiàn)程導致了死鎖
但是,本書(shū)中的程序一點(diǎn)也不復雜(關(guān)于線(xiàn)程處理這一點(diǎn)),因此死鎖不應該是什么大問(wèn)題。
以前已經(jīng)提到過(guò)數據損壞,但在多線(xiàn)程處理中它是一個(gè)非常嚴重的問(wèn)題,所以有必要再次提及它。如果不使用合理的同步結構(例如,互斥鎖)來(lái)控制以獨占方式訪(fǎng)問(wèn)數據,則數據損壞就是多線(xiàn)程處理中所面臨的一個(gè)巨大問(wèn)題。必須一直能絕對保證,只有在修改數據的線(xiàn)程知道沒(méi)有其他線(xiàn)程與自己同時(shí)讀取或者寫(xiě)入數據時(shí),數據才能被修改。許多無(wú)法解釋的bug都是由于數據爭用而產(chǎn)生的。
多線(xiàn)程程序的調試是很痛苦的一件事。由于大多數調試器并不支持多線(xiàn)程,所以要一步一步執行多線(xiàn)程程序極其困難。
更糟糕的是,有些支持多線(xiàn)程的調試器允許其他線(xiàn)程正常執行,而實(shí)質(zhì)上正在調試的線(xiàn)程卻被停止。這就使得在正常的程序中正確地進(jìn)行同步極其困難。
最后,多線(xiàn)程程序是不確定的。這就意味著(zhù)線(xiàn)程的啟動(dòng)和停止是由操作系統來(lái)控制的,而我們絕對無(wú)法控制它。在單線(xiàn)程程序中,可以模擬導致游戲崩潰的環(huán)境,方法是重復輸入同樣的信息,并使用相同的隨機數生成器的種子,但是卻無(wú)法模擬線(xiàn)程的執行順序。正是由于這個(gè)原因,程序一百次可能有一次崩潰,但卻不容易跟蹤到崩潰的原因,因為導致崩潰的環(huán)境難以再現!
聯(lián)系客服