溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

C++怎么為多線程性能設(shè)計數(shù)據(jù)結(jié)構(gòu)

發(fā)布時間:2021-11-01 16:13:56 來源:億速云 閱讀:166 作者:iii 欄目:編程語言

本篇內(nèi)容主要講解“C++怎么為多線程性能設(shè)計數(shù)據(jù)結(jié)構(gòu)”,感興趣的朋友不妨來看看。本文介紹的方法操作簡單快捷,實用性強。下面就讓小編來帶大家學(xué)習(xí)“C++怎么為多線程性能設(shè)計數(shù)據(jù)結(jié)構(gòu)”吧!

8.3.1 為復(fù)雜操作劃分?jǐn)?shù)組元素

假設(shè)你正在做一些復(fù)雜的數(shù)學(xué)計算,你需要將兩個大矩陣想乘。為了實現(xiàn)矩陣相乘,你將第一個矩陣的第一行每個元素與第二個矩陣的第一列相對應(yīng)的每個元素相乘,并將結(jié)果相加得到結(jié)果矩陣左上角第一個元素。然后你繼續(xù)將第二行與第一列相乘得到結(jié)果矩陣第一列的第二個元素,以此類推。正如圖8.3所示,突出顯示的部分表明了第一個矩陣的第二行與第二個矩陣的第三列配對,得到結(jié)果矩陣的第三列第二行的值。

C++怎么為多線程性能設(shè)計數(shù)據(jù)結(jié)構(gòu)

圖8.3 矩陣相乘

為了值得使用多線程來優(yōu)化該乘法運算,現(xiàn)在我們假設(shè)這些都有幾千行和幾千列的大矩陣。通常,非稀疏矩陣在內(nèi)存中是用一個大數(shù)組表示的,第一行的所有元素后面是第二行的所有元素,以此類推。為了實現(xiàn)矩陣相乘,現(xiàn)在就有三個大數(shù)組了。為了獲得更優(yōu)的性能,你就需要注意數(shù)據(jù)存取部分,特別是第三個數(shù)組。

有很多在線程間劃分工作的方法。假設(shè)你有比處理器更多的行例,那么你就可以讓每個線程計算結(jié)果矩陣中某些列的值,或者讓每個線程計算結(jié)果矩陣中某些行的值,或者甚至讓每個線程計算結(jié)果矩陣中規(guī)則矩形子集的值。

回顧8.2.3節(jié)和8.2.4節(jié),你就會發(fā)現(xiàn)讀取數(shù)組中的相鄰元素比到處讀取數(shù)組中的值要好,因為這樣減少了緩存使用以及假共享。如果你使每個線程處理一些列,那么就需要讀取第一個矩陣中的所有元素以及第二個矩陣中相對應(yīng)的列中元素,但是你只會得到列元素的值。假設(shè)矩陣是用行順序存儲的,這就意味著你從第一行中讀取N個元素,從第二行中讀取N個元素,以此類推(N的值是你處理的列的數(shù)目)。別的線程會讀取每一行中別的元素,這就很清楚你應(yīng)該讀取相鄰的的列,因此每行的N個元素就是相鄰的,并且最小化了假共享。當(dāng)然,如果這N個元素使用的空間與緩存線的數(shù)量相等的話,就不會有假共享,因為每個線程都會工作在獨立的緩存線上。

另一方面,如果每個線程處理一些行元素,那么就需要讀取第二個矩陣中的所有元素,以及第一個矩陣中相關(guān)的行元素,但是它只會得到行元素。因為矩陣是用行順序存儲的,因此你現(xiàn)在讀取從N行開始的所有元素。如果你選擇相鄰的行,那么就意味著此線程是現(xiàn)在唯一對這N行寫入的線程;它擁有內(nèi)存中連續(xù)的塊并且不會被別的線程訪問。這就比讓每個線程處理一些列元素更好,因為唯一可能產(chǎn)生假共享的地方就是一塊的最后一些元素與下一個塊的開始一些元素。但是值得花時間確認(rèn)目標(biāo)結(jié)構(gòu)。

第三種選擇一劃分為矩形塊如何呢?這可以被看做是先劃分為列,然后劃分為行。它與根據(jù)列元素劃分一樣存在假共享問題。如果你可以選擇塊的列數(shù)目來避免這種問題,那么從讀這方面來說,劃分為矩形塊有這樣的優(yōu)點:你不需要讀取任何一個完整的源矩陣。你只需要讀取相關(guān)的目標(biāo)矩陣的行與列的值。從具體方面來看,考慮兩個1000行和1000列的矩陣相乘。就有一百萬個元素。如果你有100個處理器,那么每個線程可以處理10行元素。盡管如此,為了計算這10000個元素,需要讀取第二個矩陣的所有元素(一百萬個元素)加上第一個矩陣相關(guān)行的10000個元素,總計1010000個元素;另一方面,如果每個線程處理100行100列的矩陣塊(總計10000個元素) ,那么它們需要讀取第一個矩陣的100行元素( 100 x 1000=100000元素)和第二個矩陣的100列元素(另一個100000個元素)。這就只有200000元素,將讀取的元素數(shù)量降低到五分之一。如果你讀取更少的元素,那么發(fā)生緩存未命中和更好性能的潛力的機會就更少了。

因此將結(jié)果矩陣劃分為小的方塊或者類似方塊的矩陣比每個線程完全處理好幾行更好。當(dāng)然,你可以調(diào)整運行時每個塊的大小,取決于矩陣的大小以及處理器的數(shù)量。如果性能很重要,基于目標(biāo)結(jié)構(gòu)分析各種選擇是很重要的。

你也有可能不進(jìn)行矩陣乘法,那么它是否適用呢?當(dāng)你在線程間劃分大塊數(shù)據(jù)的時候,同樣的原則也適用于這種情況。仔細(xì)觀察數(shù)據(jù)讀取方式,并且識別影響性能的潛在原因。在你遇到的問題也可能有相似的環(huán)境,就是只要改變工作劃分方式可以提高性能而不需要改變基本算法。

好了,我們已經(jīng)看到數(shù)組讀取方式是如何影響性能的。其他數(shù)據(jù)結(jié)構(gòu)類型呢?

8.3.2其他數(shù)據(jù)結(jié)構(gòu)中的數(shù)據(jù)訪問方式

從根本上說,當(dāng)試圖優(yōu)化別的數(shù)據(jù)結(jié)構(gòu)的數(shù)據(jù)訪問模式時也是適用的。

1、在線程間改變數(shù)據(jù)分配,使得相鄰的數(shù)據(jù)被同一個線程適用。

2、最小化任何給定線程需要的數(shù)據(jù)。

3、確保獨立的線程訪問的數(shù)據(jù)相隔足夠遠(yuǎn)來避免假共享。

當(dāng)然,運用到別的數(shù)據(jù)結(jié)構(gòu)上是不容易的。例如,二叉樹本來就很難用任何方式來再分,有用還是沒用,取決于樹是如何平衡的以及你需要將它劃分為多少個部分。同樣,樹的本質(zhì)意味著結(jié)點是動態(tài)分配的,并且最后在堆上不同地方。

現(xiàn)在,使數(shù)據(jù)最后在堆上不同地方本身不是一個特別的問題,但是這意味著處理器需要在緩存中保持更多東西。實際上這可以很有利。如果多個線程需要遍歷樹,那么它們都需要讀取樹的結(jié)點,但是如果樹的結(jié)點至包含指向該結(jié)點持有數(shù)據(jù)的指針,那么當(dāng)需要的時候,處理器就必須從內(nèi)存中載入數(shù)據(jù)。如果線程正在修改需要的數(shù)據(jù),這就可以避免結(jié)點數(shù)據(jù)與提供樹結(jié)構(gòu)的數(shù)據(jù)間的假共享帶來的性能損失。

使用互斥元保護(hù)數(shù)據(jù)的時候也有同樣的問題。假設(shè)你有一個簡單的類,它包含一些數(shù)據(jù)項和一個互斥元來保護(hù)多線程讀取。如果互斥元和數(shù)據(jù)項在內(nèi)存中離得很近,對于使用此互斥元的線程來說就很好;它需要的數(shù)據(jù)已經(jīng)在處理器緩存中了,因為為了修改互斥元已經(jīng)將它載入了。但是它也有一個缺點:當(dāng)第一個線程持有豆斥元的時候,如果別的線程試圖鎖住互斥元,它們就需要讀取內(nèi)存。互斥元的鎖通常作為一個在互斥元內(nèi)的存儲單元上試圖獲取互斥元的讀一修改一寫原子操作來實現(xiàn)的,如果互斥元已經(jīng)被鎖的話,就接著調(diào)用操作系統(tǒng)內(nèi)核。這個讀一修改一寫操作可能導(dǎo)致?lián)碛谢コ庠木€程持有的緩存中的數(shù)據(jù)變得無效。只要使用互斥元,這就不是問題。盡管如此,如果互斥元和線程使用的數(shù)據(jù)共享同一個緩沖線,那么擁有此互斥元的線程的性能就會因為另一個線程試圖鎖住該互斥元而受到影響。

測試這種假共享是否是一個問題的方法就是在數(shù)據(jù)元素間增加可以被不同的線程并發(fā)讀取的大塊填充數(shù)據(jù)例如,你可以使用:

C++怎么為多線程性能設(shè)計數(shù)據(jù)結(jié)構(gòu)

來測試互斥元競爭問題或者使用:

C++怎么為多線程性能設(shè)計數(shù)據(jù)結(jié)構(gòu)

來測試數(shù)組數(shù)據(jù)是否假共享。如果這樣做提高了性能,就可以得知假共享確實是一個問題,并且你可以保留填充數(shù)據(jù)或者通過重新安排數(shù)據(jù)讀取的方式來消除假共享。

當(dāng)然,當(dāng)設(shè)計并發(fā)性的時候,不僅需要考慮數(shù)據(jù)讀取模式,因此讓我們來看看別的需要考慮的方面。

8.4 為并發(fā)設(shè)計時的額外考慮

本章我們看了一些在線程間劃分工作的方法,影響性能的因素,以及這些因素是如何影響你選擇哪種數(shù)據(jù)讀取模式和數(shù)據(jù)結(jié)構(gòu)的。但是,設(shè)計并發(fā)代碼需要考慮更多。你需要考慮的事情例如異常安全以及可擴展性。如果當(dāng)系統(tǒng)中處理核心增加時性能(無論是從減少執(zhí)行時間還是從增加吞吐量方面來說)也增加的話,那么代碼就是可擴展的。從理論上說,性能增加是線性的。因此一個有100個處理器的系統(tǒng)的性能比只有一個處理器的系統(tǒng)好100倍。

即使代碼不是可擴展的,它也可以工作。例如,單線程應(yīng)用不是可擴展的,異常安全是與正確性有關(guān)的。如果你的代碼不是異常安全的,就可能會以破碎的不變量或者競爭條件結(jié)束,或者你的應(yīng)用可能因為一個操作拋出異常而突然終止??紤]到這些,我們將首先考慮異常安全。

8.4.1 并行算法中的異常安全

異常安全是好的C++代碼的一個基本方面,使用并發(fā)性的代碼也不例外。實際上,并行算法通常比普通線性算法需要你考慮更多關(guān)于異常方面的問題。如果線性算法中的操作拋出異常,該算法只要考慮確保它能夠處理好以避免資源泄漏和破碎的不變量。它可以允許擴大異常給調(diào)用者來處理。相比之下,在并行算法中,很多操作在不同的線程上運行。在這種情況下,就不允許擴大異常了,因為它在錯誤的調(diào)用棧中。如果一個函數(shù)大量產(chǎn)生以異常結(jié)束的新線程,那么該應(yīng)用就會被終止。

作為一個具體的例子,我們來回顧清單2.8中的 parallel_accumulate函數(shù),清單8.2中會做一些修改

清單8.2 sta::accumulate的并行版本(來自清單2.8)

C++怎么為多線程性能設(shè)計數(shù)據(jù)結(jié)構(gòu)

現(xiàn)在我們檢查并且確定拋出異常的位置:總的說來,任何調(diào)用函數(shù)的地方或者在用戶定義的類型上執(zhí)行操作的地方都可能拋出異常。

首先,你調(diào)用distance 2,它在用戶定義的迭代器類型上執(zhí)行操作。因為你還沒有進(jìn)行任何工作,并且這是在調(diào)用線程上,所以這是沒問題的。下一步,你分配了results選代器3和threads迭代器4。同樣,這是在調(diào)用線程上,并且你沒有做任何工作或者生產(chǎn)任何線程,因此這是沒問題的。當(dāng)然,如果threads構(gòu)造函數(shù)拋出異常,那么就必須清楚為results分配的內(nèi)存,析構(gòu)函數(shù)將為你完成它。

跳過block_start 的初始化5因為這是安全的,就到了產(chǎn)生線程的循環(huán)中的操作6、7、8。一旦在7中創(chuàng)造了第一個線程,如果拋出異常的話就會很麻煩,你的新sta::thread 對象的析構(gòu)函數(shù)會調(diào)用

std::terminate 然后中程序,

調(diào)用accumulate_block 9也可能會拋出異常,你的線程對象將被銷毀并且調(diào)用std:terminate ;另一方面,最后調(diào)用std::accumulate 10的時候也可能拋出異常并且不導(dǎo)致任何困難,因為所有線程將在此處匯合。

這不是對于主線程來說的,但是也可能拋出異常,在新線程上調(diào)用 accumulate_block 可能拋出異常1。這里沒有任何catch塊,因此該異常將被稍后處理并且導(dǎo)致庫調(diào)用sta::terminate()來中止程序。

即使不是顯而易見的,這段代碼也不是異常安全的。

1·增加異常安全性

好了,我們識別出了所有可能拋出異常的地方以及異常所造成的不好影響。那么如何處理它呢?我們先來解決在新線程上拋出異常的問題。

在第4章中介紹了完成此工作的工具。如果你仔細(xì)考慮在新線程中想獲得什么,那么很明顯當(dāng)允許代碼拋出異常的時候,你試圖計算結(jié)果來返回。std: :packaged_task 和std:future 的組合設(shè)計是恰好的。如果你重新設(shè)計代碼來使用 std::packaged_task ,就以清單8.3中的代碼結(jié)束。

清單8.3使用std::packaged_task的std::accumulate的并行版本

C++怎么為多線程性能設(shè)計數(shù)據(jù)結(jié)構(gòu)

C++怎么為多線程性能設(shè)計數(shù)據(jù)結(jié)構(gòu)

第一個改變就是,函數(shù)調(diào)用accumulate_block操作直接返回結(jié)果,而不是返回存儲地址的引用1。你使用std::packaged_task 和std::future來保證異常安全,因此你也可以使用它來轉(zhuǎn)移結(jié)果。這就需要你調(diào)用std::accumulate 2明確使用默認(rèn)構(gòu)造函數(shù)T而不是重新使用提供的result值,不過這只是一個小小的改變。

下一個改變就是你用futures 向量3,而不是用結(jié)果為每個生成的線程存儲一個 std:future 。在生成戈程的循環(huán)中,你首先為 accumulate_block 創(chuàng)造一個任務(wù)4。std:packaged_task

既然你已經(jīng)使用了future ,就不再有結(jié)果數(shù)組了,因此必須將最后一塊的結(jié)果存儲在一個變量中7而不是存儲在數(shù)組的一個位置中。同樣,因為你將從future中得到值,使用基本的for 循環(huán)比使用std:accumulate要簡單,以提供的初始值開始8,并且將每個future的結(jié)果累加起來9。如果相應(yīng)的任務(wù)拋出異常,就會在future中捕捉到并且調(diào)用get() 時會再次拋出異常。最后,在返回總的結(jié)果給調(diào)用者之前要加上最后一個塊的結(jié)果10。

因此,這就去除了一個可能的問題,工作線程中拋出的異常會在主線程中再次被拋出。如果多于一個工作線程拋出異常,只有一個異常會被傳播,但是這也不是一個大問題。如果確實有關(guān)的話,可以使用類似

std::nested_exception來捕捉所有的異常然后拋出它。

如果在你產(chǎn)生第一個線程和你加入它們之間拋出異常的話,那么剩下的問題就是線程泄漏。最簡單的方法就是捕獲所有異常,并且將它們?nèi)诤系秸{(diào)用joinable()的線程中,然后再次拋出異常。

C++怎么為多線程性能設(shè)計數(shù)據(jù)結(jié)構(gòu)

現(xiàn)在它起作用了。所有線程將被聯(lián)合起來,無論代碼是如何離開塊的。盡管如此, try-catch塊是令人討厭的,并且你有復(fù)制代碼。你將加入正常的控制流以及捕獲塊的線程中。復(fù)制代碼不是一個好事情,因為這意味著需要改變更多的地方。我們在一個對象的析構(gòu)函數(shù)中檢查它,畢竟,這是C++中慣用的清除資源的方法。下面是你的類。

C++怎么為多線程性能設(shè)計數(shù)據(jù)結(jié)構(gòu)

這與清單2.3中的thread_guard類是相似的,除了它擴展為適合所有線程。你可以如清單8.4所示簡化代碼。

清單8.4 std::accumulate的異常安全并行版本

C++怎么為多線程性能設(shè)計數(shù)據(jù)結(jié)構(gòu)

C++怎么為多線程性能設(shè)計數(shù)據(jù)結(jié)構(gòu)

一旦你創(chuàng)建了你的線程容器,也就創(chuàng)建了一個新類的實例1來加入所有在退出的線程。你可以去除你的聯(lián)合循環(huán),只要你知道無論函數(shù)是否退出,這些線程都將被聯(lián)合起來。注意調(diào)用 futures[i].get() 2將被阻塞直到結(jié)果出來,因此在這一點并不需要明確地與線程融合起來。這與清單8.2中的原型不一樣,在清單8.2中你必須與線程聯(lián)合起來確保正確復(fù)制了結(jié)果向量。你不僅得到了異常安全代碼,而且你的函數(shù)也更短了,因為將聯(lián)合代碼提取到你的新(可再用的)類中了。

2. STD::ASYNC()的異常安全

你已經(jīng)知道了當(dāng)處理線程時需要什么來實現(xiàn)異常安全,我們來看看使用std::async() 時需要做的同樣的事情。你已經(jīng)看到了,在這種情況下庫為你處理這些線程,并且當(dāng)future是就緒的時候,產(chǎn)生的任何線程都完成了。需要注意到關(guān)鍵事情就是異常安全,如果銷毀future的時候沒有等待它,析構(gòu)函數(shù)將等待線程完成。這就避免了仍然在執(zhí)行以及持有數(shù)據(jù)引用的泄漏線程的問題。清單8.5所示就是使用std::async ()的異常安全實現(xiàn)。

清單8.5 使用std::async的std::accumulate的異常安全并行版本

C++怎么為多線程性能設(shè)計數(shù)據(jù)結(jié)構(gòu)
C++怎么為多線程性能設(shè)計數(shù)據(jù)結(jié)構(gòu)

這個版本使用遞歸將數(shù)據(jù)劃分為塊而不是重新計算將數(shù)據(jù)劃分為塊,但是它比之前的版本要簡單一些,并且是異常安全的。如以前一樣,你以計算序列長度開始1,如果它比最大的塊尺寸小的話,就直接調(diào)用

std::accumuate 2。如果它的元素比塊尺寸大的話,就找到中點3然后產(chǎn)生一個異步任務(wù)來處理前半部分![序號4。范圍內(nèi)的第二部分就用一個直接的遞歸調(diào)用來處理5。,然后將這兩個塊的結(jié)果累加6。庫確保了std::async調(diào)用使用了可獲得的硬件線程,并且沒有創(chuàng)造很多線程。一些"異步”調(diào)用將在調(diào)用get()的時候被異步執(zhí)行6。

這種做法的好處在于它不僅可以利用硬件并發(fā),而且它也是異常安全的。如果遞歸調(diào)用拋出異常5,當(dāng)異常傳播時,調(diào)用std::async 4創(chuàng)造的future就會被銷毀。它會輪流等待異步線程結(jié)束,因此避免了懸掛線程。另一方面,如果異步調(diào)用拋出異常,就會被future捕捉,并且調(diào)用get() 6將再次拋出異常。

當(dāng)設(shè)計并發(fā)代碼的時候還需要考慮哪些方面呢?讓我們來看看可擴展性。如果將你的代碼遷移到更多處理器系統(tǒng)中會提高多少性能呢?

8.4.2可擴展性和阿姆達(dá)爾定律

可擴展性是關(guān)于確保你的應(yīng)用可以利用系統(tǒng)中增加的處理器。一種極端情況就是你有一個完全不能擴展的單線程應(yīng)用,即使你在系統(tǒng)中增加100個處理器也不會改變性能。另一種極端情況是你有類似SETI@Home[3]的項目,被設(shè)計用來利用成千上萬的附加的處理器(以用戶將個人計算機增加到網(wǎng)絡(luò)中的形式)。

對于任何給定的多線程程序,當(dāng)程序運行時,執(zhí)行有用工作的線程的數(shù)量會發(fā)生變化。即使每個線程都在做有用的操作,初始化應(yīng)用的時候可能只有一個線程,然后就有一個任務(wù)生成其他的線程。但是即使那樣也是一個不太可能發(fā)生的方案。線程經(jīng)?;〞r間等待彼此或者等待I/O操作完成

除非每次線程等待事情(無論是什么事情)的時候都有另一個線程在處理器上代替它,否則就有一個可以進(jìn)行有用工作的處理器處于閑置狀態(tài)

一種簡單的方法就是將程序劃分為只有一個線程在做有用的工作"串行的"部分和所有可以獲得的處理器都在做有用工作的"并行的"部分。如果你在有更多處理器的系統(tǒng)上運行你的應(yīng)用,理論上就可以更快地完成"并行"部分,因為可以在更多的處理器間劃分工作,但是"串行的"部分仍然是串行的。在這樣一種簡單假設(shè)下,你可以通過增加處理器數(shù)量來估計可以獲得的性能。如果“連續(xù)的"部分組成程序的一個部分fs,那么使用N個處理器獲得的性能P就可以估計為

C++怎么為多線程性能設(shè)計數(shù)據(jù)結(jié)構(gòu)

這就是阿姆達(dá)爾定律(Amdahl'slaw ) ,當(dāng)談?wù)摬l(fā)代碼性能的時候經(jīng)常被引用。如果所有事情都能被并行,那么串行部分就為0,加速就是N,或者,如果串行部分是三分之一,即使有無限多的處理器,你也不會得到超過3的加速

盡管如此,這是一種很理想的情況。因為任務(wù)很少可以像方程式所需要的那樣被無窮劃分,并且所有事情都達(dá)到它所假設(shè)的CPU界限是很少出現(xiàn)的。正如你看到的,線程執(zhí)行的時候會等待很多事情。

阿姆達(dá)爾定律中有一點是明確的,那就是當(dāng)你為性能使用并發(fā)的時候,值得考慮總體應(yīng)用的設(shè)計來最大化并發(fā)的可能性,并且確保處理器始終有有用的工作來完成。如果你可以減少“串行”部分或者減少線程等待的可能性,你就可以提高在有更多處理器的系統(tǒng)上的性能。或者,如果你可以為系統(tǒng)提供更多的數(shù)據(jù),并且保持并行部分準(zhǔn)備工作,就可以減少串行部分,增加性能P的值。

從根本上說,可擴展性就是當(dāng)增加更多的處理器的時候,可以減少它執(zhí)行操作的時間或者增加在一段時間內(nèi)處理的數(shù)據(jù)數(shù)量。有時這兩點是相同的(如果每個元素可以處理得更快,那么你就可以處理更多數(shù)據(jù)) ,但是并不總是一樣的。在選擇在線程間劃分工作的方法之前,識別出可擴展性的哪些方面對你很重要是很必要的。

在這部分的開始我就提到過線程并不是總有有用的工作來做。有時它們必須等待別的線程,或者等待I/O操作完成,或者別的事情。如果在等待中你給系統(tǒng)一些有用的事情,你就可以有效的"隱藏等待。

8.4.3用多線程隱藏遲

在很多關(guān)于多線程代碼性能的討論中,我們都假設(shè)當(dāng)它們真正在處理器上運行時,線程在"全力以赴的運行并且總是有有用的工作來做。這當(dāng)然不是正確的,在應(yīng)用代碼中,線程在等待的時候總是頻繁地被阻塞。例如,它們可能在等待一些I/O操作的完成,等待獲得互斥元,等待另一個線程完成一些操作并且通知一個條件變量,或者只是休眠一段時間。

無論等待的原因是什么,如果你只有和系統(tǒng)中物理處理單元一樣多的線程,那么有阻塞的線程就意味著你在浪費CPU時間。運行一個被阻塞的線程的處理器不做任何事情。因此,如果你知道一個線程將會有相當(dāng)一部分時間在等待,那么你就可以通過運行一個或多個附加線程來使用那個空閑的CPU時間。

考慮一個病毒掃描應(yīng)用,它使用管道在線程間劃分工作。第一個線程搜索文件系統(tǒng)來檢查文件并且將它們放到隊列中。同時,另一個線程獲得隊列中的文件名,載入文件,并且掃描它們的病毒。你知道搜索文件系統(tǒng)的文件來掃描的線程肯定會達(dá)到I/O界限,因此你就可以通過運行一個附加的掃描線程來使用“空閑的"CPU時間。那么你就有一個搜索文件線程,以及與系統(tǒng)中的物理核或者處理器相同數(shù)量的掃描線程。因為掃描線程可能也不得不從磁盤讀取文件的重要部分來掃描它們,擁有更多掃描線程也是很有意義的。但是在某個時刻會有太多線程,系統(tǒng)會再次慢下來因為它花了更多時間切換程序,正如8.2.5節(jié)所描述的。

仍然,這是一個最優(yōu)化問題,因此測量線程數(shù)量改變前后的性能時很重要的;最有的線程數(shù)量將很大程度上取決于工作的性質(zhì)和線程等待的時間所占的比例。

取決于應(yīng)用,它也可能用完空閑的CPU時間而沒有運行附加的線程。例如,如果一個線程因為等待I/O操作的完成而被阻塞,那么使用可獲得的異步I/O操作就很有意義了。那么當(dāng)在背后執(zhí)行I/O操作的時候,線程就可以執(zhí)行別的有用的工作了。在別的情況下,如果一個線程在等待另一個線程執(zhí)行一個操作,那么等待的線程就可以自己執(zhí)行那個操作而不是被阻塞,正如第7章中的無鎖隊列。在一個極端的情況下,如果線程等待完成一個任務(wù)并且該任務(wù)沒有被其他線程執(zhí)行,等待的線程可以在它內(nèi)部或者另一個未完成的任務(wù)中執(zhí)行這個任務(wù)。清單8.1中你看到了這個例子,在排序程序中只要它需要的塊沒有排好序就不停地排序它。

有時它增加線程來確保外部事件及時被處理來增加系統(tǒng)響應(yīng)性,而不是增加線程來確保所有可得到的處理器都被應(yīng)用了。

8.4.4 用并發(fā)提高響應(yīng)性

很多現(xiàn)代圖形用戶接口框架是事件驅(qū)動的,使用者通過鍵盤輸入或者移動鼠標(biāo)在用戶接口上執(zhí)行操作,這會產(chǎn)生一系列的事件或者消息,稍后應(yīng)用就會處理它。系統(tǒng)自己也會產(chǎn)生消息或者事件。為了確保所有事件和消息都能被正確處理,通常應(yīng)用都有下面所示的一個事件循環(huán)。

C++怎么為多線程性能設(shè)計數(shù)據(jù)結(jié)構(gòu)

顯然, API的細(xì)節(jié)是不同的,但是結(jié)構(gòu)通常是一樣的,等待一個事件,做需要的操作來處理它,然后等待下一個事件。如果你有單線程應(yīng)用,就會導(dǎo)致長時間運行的任務(wù)很難被寫入,如8.1.3節(jié)描述的。為了確保用戶輸入能被及時處理, get_event()和process()必須以合理的頻率被調(diào)用,無論應(yīng)用在做什么操作。這就意味著要么任務(wù)必須定期暫停并且將控制交給事件循環(huán),要么方便的時候在代碼中調(diào)用get_event()/

process()代碼。每一種選擇都將任務(wù)的實現(xiàn)變得復(fù)雜了。

通過用并發(fā)分離關(guān)注點,你就可以將長任務(wù)在一個新線程上執(zhí)行,并且用一個專用的GUI線程來處理事件。線程可以通過簡單的方法互相訪問,而不是必須以某種方式將事件處理代碼放到任務(wù)代碼中。清單8.6列出了這種分離的簡單概括。

清單8.6從任務(wù)線程中分離GUI線程

C++怎么為多線程性能設(shè)計數(shù)據(jù)結(jié)構(gòu)

C++怎么為多線程性能設(shè)計數(shù)據(jù)結(jié)構(gòu)

通過這種方式分離障礙,用戶線程能夠及時地響應(yīng)事件,即使任務(wù)要執(zhí)行很長時間。這種響應(yīng)性通常是使用應(yīng)用時用戶體驗的關(guān)鍵。無論何時執(zhí)行一個特定操作(無論該操作是什么) ,應(yīng)用都會被完全鎖住,這樣使用起來就不方便了。通過提供一個專用的事件處理線程, GUI可以處理GUI特有的消息(例如調(diào)整窗口大小或者重畫窗口)而不會中斷耗時處理的執(zhí)行,并且當(dāng)它們確實影響長任務(wù)時會傳遞相關(guān)的消息。

到此,相信大家對“C++怎么為多線程性能設(shè)計數(shù)據(jù)結(jié)構(gòu)”有了更深的了解,不妨來實際操作一番吧!這里是億速云網(wǎng)站,更多相關(guān)內(nèi)容可以進(jìn)入相關(guān)頻道進(jìn)行查詢,關(guān)注我們,繼續(xù)學(xué)習(xí)!

向AI問一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。

c++
AI