歡迎您光臨本站 註冊首頁

Solaris2.4 多線程編程指南7--編程指南

←手機掃碼閱讀     火星人 @ 2014-03-12 , reply:0
  本文出自:BBS水木清華站 作者:Mccartney (coolcat) (2002-01-29 20:33:46)

7 編程指南

本章給出線程編程的一些要點。特彆強調了單線程和多線程編程方法的差別。
重新認識全局變數
提供靜態局部變數
線程同步
避免死鎖
一些基本的注意事項
用多處理器編程

在歷史上,大多數代碼以單線程的方式來編程。如果在C程序里調用庫函數則
尤其是這樣:
· 如果你給全局變數賦值,並且在一會以後讀該變數,則讀的結果和寫的是一樣的。
· 對於非全局的,靜態存儲也是如此
· 不需要同步機制,因為沒有什麼可以同步的
在下面的幾個多線程例子當中討論了採用以上假設將會發生的問題,以及你
如何處理這些問題。

7.1重新認識全局變數

傳統情況下,單線程C和UNIX有處理系統調用錯誤的傳統。系統調用可以返回
任何值(例如,write()返回傳輸的位元組數)作為功能性的返回值。然而,-1被保留,
它意味著出錯。所以,如果一個系統調用返回-1,你就知道是失敗了。

Code Example 7-1 全局變數和錯誤碼errno

Extern int errno;

if(write(file_desc,buffer,size)==-1){
/* the system call failed */
fprintf(stderr,"something went wrong, error code = %d\n",errno);
exit(1);
}

函數並不直接返回錯誤碼(將會和正常的返回值混淆),而是將錯誤碼放入一個
名為errno的全局變數中。如果一個系統調用失敗,你可以讀errno來確定問題所在。
現在考慮在多線程程序中,兩個線程幾乎同時失敗,但錯誤碼不同。他們都希望
在errno中尋找問題,但一個errno不能存放兩個值。這個全局變數不能被多線程程序
使用。
Solaris線程包通過一種在概念上全新的存儲類型解決了這個問題--線程專有數據。
與全局變數類似,在線程運行時任何過程都可以訪問這塊內存。然而,它是線程私有
的--如果兩個線程參考同名的線程專有存儲區,它們實際上是兩個存儲區。
所以,如果使用線程,每個對errno的操作是線程專有的,因為每個線程有一個
errno的私有拷貝。

7.2提供給靜態局部變數

示例7-2顯示了一個類似與errno錯誤的問題,但涉及到靜態存儲,而不是全局存
儲。Gethostbyname(3N)函數用計算機名稱作為參數。返回值是一個指向結構的指針,
該結構包含通過網路訪問指定計算機的必要信息。

Code Example 7-2 gethostbyname問題

Struct hostent *gethostbyname(char *name){
Static struct hostent result;
/*lookup name in hosts database */
/*put answer in reault*/
return(&result);
}
返回指向自動局部變數不是一個好的選擇,儘管在這個例子是可以的,因為所指
定的變數是靜態的。但是,如果兩個線程用不同的計算機名同時訪問這個區域,對靜
態存儲的使用就會發生衝突。
線程專有數據可以代替靜態儲存,就象在errno問題中那樣,但是這涉及到動態
分配內存,並且增加了調用的開銷。
一個更好的辦法是調用者提供存放數據的存儲區。這需要在函數調用中增加一個
參數,一個輸出參數。即需要一個gethostbyname的新的版本。
在Solaris里,這種技術被用來處理很多類似問題。在大多數情況下,新介面的
名字都帶有"_r"後綴,例如gethostbyname_r(3N)。

7.3線程同步

應用程序中的線程在處理共享數據和進程資源是必須使用同步機制。
在多個線程式控制制一個對象時會出現一個問題。在單線程世界里,對這些對象的同
步訪問不是問題,但正如示例7-3所示,在多線程編程中需要注意。(注意Solaris
printf(3S)對多線程程序是安全的;此例說明如果printf不安全將會發生的問題。)

Code Example 7-3 printf()問題
/*thread 1*/
printf("go to statement reached");

/*thread 2*/
printf("hello world");

printed on display:
go to hello

7.3.1單線程策略

一個辦法是採用單一的,應用程序範圍有效的互斥鎖,在調用printf時必須使用
互斥鎖保護。因為每次只有一個線程可以訪問共享數據,每個線程所見的內存是一致
的。
Because this is effectively a single-threaded program, very little is
gained bythis strategy.

7.3.2重入(reentrant)函數

更好的辦法是採用模塊化和數據封裝的思想。一個替代函數被提供,在被幾個線
程同時調用時是安全的。寫一個替代函數的關鍵是搞清楚什麼樣的操作是"正確的"。
可以被幾個線程調用的函數一定是重入的。這也許需要改變函數介面的實現。
訪問全局狀態的函數,例如內存和文件,都存在重入問題。這些函數需要用
Solaris線程提供的正確的同步機制來保護自己訪問全局狀態。
兩個保證函數重入的基本策略是代碼鎖和數據鎖。

7.3.2.1代碼鎖

代碼鎖是函數調用級的策略,它保證函數完全在鎖的保護下運行。該策略假設對
數據的所有訪問都是通過函數。共享數據的函數應當在同一個鎖的保護下執行。
有些并行編程語言提供一種名為監視器(monitor)的機制,在monitor的內部,
函數的代碼被隱含地用所來保護。一個monitor也可以用互斥鎖實現

7.3.2.2數據鎖

數據鎖保證對數據集合(collection of data)維護的一致性。對於數據鎖,仍
然有代碼鎖的概念,但代碼鎖是僅僅圍繞訪問共享數據進行。對於一個互斥鎖協議,
僅有一個線程來操作每一個數據集合。???
在多讀單寫協議當中,幾個讀操作或一個寫操作可以被允許。在操作不同的數據
集合,或者在同一個數據集合上不違反多讀單寫的協議的前提下,一個模塊中的多個
線程可以同時執行。所以,數據鎖比代碼鎖提供了更多的同時性。
如果你需要使用鎖的話,你要用哪一種(互斥鎖,條件變數,信號量)呢?你需
要嘗試只在必要時加鎖來允許更多的併發呢(fine-grained locking 細紋鎖),還
是使鎖在相當一段時間內有效來避免加鎖和釋放鎖的額外開銷呢(coarse-grained
locking 粗紋鎖)?
鎖的紋理(可以理解成加鎖和釋放鎖的頻率,頻率越高則紋理越細--譯者注)依
賴於所保護的數據量。一個粗紋鎖可以是一個保護所有數據的單一的鎖。把數據由適
當數量的鎖分開來保護是很重要的。如果紋理過細可能會影響性能,過多的加鎖和解
鎖操作會累計到相當的程度。
普通的做法是:用一個粗紋鎖開始,找到限制性能的瓶頸,然後在需要時加入細
紋鎖來減緩瓶頸。看上去這是一個合理的辦法,但你需要自己判斷來達到最好效果。

7.3.2.3不變數

不論是代碼鎖還是數據鎖,不變數對於控制鎖的複雜度都具有重要的意義。一個
不變數是一個永真的條件或關係。
這個定義在應用在同時執行時需要進行一定的修改:一個不變數是一個永真的條
件或關係,如果相關的鎖尚未設置。一旦鎖被設置,不變數就可能為假。然而,擁有
鎖的代碼在釋放所前一定要重新建立不變數。
一個不變數也可以是永真的條件或關係,如果鎖尚未設置。條件變數可以被認為
擁有一個不變數,那就是它的條件。

Code Example7-4 用assert(3X)來測試不變數

mutex_lock(&lock);
while(condition)
cond_wait(&cv);
assert((condition)==TRUE);
.
.
.
mutex_unlock();

Assert()命令是測試不變數的。Cond_wait()函數不保護不變數,所以線程返回
時一定要重新估價不變數。
另外一個例子是一個控制雙鏈表元素的模塊。對鏈表中每一個組件,一個好的不
變數是指向前項的指針,以及指向其後項的指針。
假設這個模塊使用代碼鎖,即僅僅用一個全局互斥鎖進行保護。如果某一項被刪
除或者某一項被添加,將會對指針進行正確操作,然後釋放互斥鎖。顯然,在操作指
針的某種意義上不變數為假,但在互斥鎖被釋放之前不變數會被重新建立。

7.4避免死鎖

死鎖是一系列線程競爭一系列資源產生的永久阻塞。某些線程可以運行並不說明
其它線程沒有死鎖。
導致死鎖的最常見的錯誤是自死鎖(self deadlock)或者遞歸死鎖(recursive
deadlock):一個線程在擁有一個鎖的情況下試圖再次獲得該鎖。遞歸死鎖是編程
時非常容易發生的錯誤。
例如,如果一個代碼監視器在調用期間讓每一個模塊的函數都去獲得互斥鎖,那
么任何在被互斥鎖保護的模塊之間調用的函數都將立即導致死鎖。如果一個函數調用
模塊以外的一些代碼,而這些代碼通過一個複雜或簡單的路徑,又反過來調用該模塊
內部被同一互斥鎖保護的函數,也會發生死鎖。
解決這種死鎖的辦法是避免調用模塊以外的函數,如果你並不知道它們是否會在
不重建不變數的情況下回調本模塊並且在調用之前丟棄所有已獲得的鎖。當然,在調
用完成後鎖會重新獲得,一定要檢查狀態以確定想要進行的操作仍然合法。
死鎖的另外一種情況是,線程1和線程2分別獲得互斥鎖A和互斥鎖B。這時線程1
想獲得互斥鎖B,而同時線程2想獲得互斥鎖A。結果,線程1阻塞等待B,而線程2阻塞
等待A,造成死鎖。
這類死鎖可以通過為互斥鎖編排順序來避免(鎖的等級 lock hierarchy)。如
果所有線程通過指定順序申請互斥鎖,死鎖就不會發生。
為鎖定義順序並非最優的做法。如果線程2在擁有互斥鎖B時對於模塊的狀態有很
多的假設,則放棄互斥鎖B來申請互斥鎖A,然後再按照順序重新申請互斥鎖B將導致
這些假設失去意義,而不得不重新估價模塊的狀態。
阻塞同步原語通常有不阻塞的版本,例如mutex_trylock()。它允許線程在沒有
競爭時打破鎖等級。如果有競爭,已經獲得的鎖通常要釋放,然後按照順序來申請。

7.4.1死鎖調度

因為鎖的獲得沒有順序的保證,一個線程編程的普遍問題是一個特定線程永遠不
會得到一個鎖(通常是條件變數),即使它看上去應當獲得。
這通常發生在擁有互斥鎖的線程釋放了鎖,在一段時間之後又重新獲得了這個鎖。
因為鎖被釋放了,似乎其他線程會獲得這個鎖。但是因為沒有誰能阻塞這個已經獲得
了鎖的線程,它就繼續執行到重新獲得互斥鎖的時候,這樣其他線程無法進行。
通常可以用在重新獲得鎖之前調用thr_yield(3T)來解決這一類型的問題。它允
許其它線程運行並獲得鎖。
因為應用程序需要的時間片變化很大,線程庫不能做強制規定。只有調用
thr_yield()來保證線程按你需要的那樣共享資源。

7.4.2加鎖的注意事項

以下是有關鎖的一些簡單的注意事項。
· 在長時間的操作(例如I/O)上盡量不要加鎖,這樣會對性能造成負影響。
· 在調用可能重新進入本模塊的函數時不要加鎖。
· 不要嘗試極端的處理器同時性。在不涉及系統調用和I/O操作的情況下,鎖通常只
被線程佔有很短的時間,衝突是很少發生的。只有在確知競爭情況時,才能長時間占
有一個鎖。
· 如果使用多個鎖,用鎖等級來防止死鎖。

7.5遵循基本的注意事項

· 搞清你引入的內容以及它們是否安全。
一個線程程序不能任意進入非線程代碼。
· 只有在初始線程中線程代碼才可以調用非安全代碼。
這保證了與初始線程關聯的靜態存儲只能被該線程使用。
· Sun提供的庫如果沒有明確地標識為unsafe,則被定義為safe。
如果man page不聲稱函數是MT-Safe的,則它是安全的。所有的MT-unsafe函
數都在man page里明確標出。
· 使用編譯標誌來控制二進位不兼容的源代碼改變。
在編譯時指定-D_REENTRANT或者保證在頭文件中定義_REENTRANT。
· 如果一個庫是多線程安全的,不要將全局進程操作線程化。???
不要把全局操作(或者有可能影響全局的操作)改成線程風格。例如,如果
文件的I/O操作被設置為線程級的操作,多個線程將不能正確訪問文件。
對於線程級的操作,或者thread cognizant的操作,要使用線程工具。例如,如
果main()函數僅僅終止正在退出main函數的那個線程,則main()函數的結尾應當為
thr_exit();
/*not reached */

7.5.1創建線程

Solaris線程包對線程數據結構,堆棧以及LWP設置緩存,這樣重複創建非綁定線
程開銷會降低。
非綁定線程的創建比其進程創建或者綁定線程的創建來開銷都要小的多。實際上,
這種開銷和從一個線程切換到另外一個線程的開銷是相當的。
所以,在需要時不斷地創建和清除線程,比起維護一個等待獨立任務的線程池通
常要划算一些。
一個好的例子是,一個RPC伺服器的工作方式是為監聽到的每一個請求創建一個
線程,並且在提供服務后清除這個線程,而不是維護很多線程來提供服務。
雖然線程創建比進程創建開銷要小,但是比起幾條指令來代價並不低。因此僅在
需要執行幾千條以上機器指令時才考慮創建線程。

7.5.2線程同時性

在預設狀態下,Solaris線程通過對非綁定線程調整執行資源(LWP)來實現對實
際活動的線程數的匹配。如果說Solaris線程包不能進行完美的調度,它至少可以保證
進程繼續運行。
如果你需要讓一定數量的線程同時處於活動狀態(執行代碼或系統調用),需要
通過thr_setconcurrency(3T)來通知線程庫。

例如:
· 如果一個資料庫伺服器給每一個用戶開設一個服務線程的話,它應當把期望得到的
用戶數目告訴操作系統Solaris。
· 如果一個窗口伺服器給每一個客戶開設一個線程,它應當把期望的活動客戶端的
數目通知Solaris。
· 一個文件拷貝程序擁有一個讀線程和一個線程,它應當通知Solaris它的同時性等級
為2。
或者,同時性等級可以在創建線程時使用THR_NEW_LWP標誌來增加。
在計算線程的同時性時,需要把因為進程間的同步變數而處於阻塞狀態的線程考慮
進來。

7.5.3效率

用thr_create(3T)創建一個新線程比重新啟動一個線程花費的時間少。這意味
著,在需要時創建一個線程並且在任務結束後用thr_exit(3T)立刻殺掉它,比起維護一
大堆的空閑線程並且在它們中間切換要划算的多。

7.5.4綁定線程

綁定線程比起非綁定線程的開銷要大。因為綁定線程可以改變它所在的LWP的屬性,
LWP在綁定線程退出后不會被緩存,在新的綁定線程生成時,操作系統將提供一個新的LWP。
僅僅在線程需要只有在所在的LWP內可用的資源時(例如虛擬的定時器或者一個指定
的堆棧),或者為了實現實時調度而必須使線程對於內核可見的場合下,才需要使用綁
定線程。
即使在你希望所有的線程都同時活動時,你也應當使用非綁定線程。因為非綁定線程
允許Solaris高效率地分配系統資源。

7.5.5線程創建指南

在使用線程時有如下簡單的注意事項:
· 在有多個執行大量任務的操作時,使用多線程編程。
· 使用線程來更好地利用CPU的同時性。
· 只有在不得已的情況下再使用綁定線程,就是說,需要LWP的特殊支持的時候。
使用thr_setconcurrency(3T)來告訴Solaris你希望有多少個線程同時執行。

7.6關於多處理器

Solaris線程包使你能夠充分利用多處理器。在很多情況下,程序員必須關心程序
是在單處理器還是在多處理器的環境下運行。
這樣的情況下涉及到多處理器的內存模型。你不能夠假設一個處理器對內存所做的
改變可以被另一個處理器立即看到。
另一個與多處理器有關的問題是如何實現"多個線程在到達同一點后再向下執行"的
有效同步。
--------------------------------------
注意-如果同步原語已經被應用於共享的內存,這裡討論的問題將不重要。
--------------------------------------

7.6.1基本建構

如果多個線程對共享存儲區的訪問使用的是Solaris的線程同步函數,那麼程序在
多處理器的環境和單處理器的環境下的運行效果是一樣的。
然而,在很多情況下,有些程序員希望更充分地發揮多處理器的優勢,希望使用一
些"巧妙"的辦法避開線程同步函數。如示例7-5和7-6所示,這樣的辦法是危險的。
了解一般的多處理器結構支持的內存模型有助於了解這種危險。
主要的多處理器組件是:

處理器本身
CPU緩衝區(Store buffers),它連接處理器和其高速緩存(caches)
高速緩存(caches),保存最近訪問和修改過的存儲地址
內存(memory),主要存儲器,被所有的處理器共享

在簡單的傳統模型里,多處理器的操作就象是直接與內存打交道:一個處理器A
向一個內存單元寫數后,另一個處理器B立刻讀取該單元,取出的數一定是處理器A剛
剛寫入的。高速緩存可以被用來加快平均的內存訪問速度,如果高速緩存之間保持一
致的話,的確可以達到期望的效果。
這種簡單方法的一個問題在於,處理器必須有一定的延遲來保證期望的語義效果
實現。許多新的多處理器結構使用各種辦法來減少這種延遲,結果不得不改變了內存
模型的語義。在如下的兩個例子當中,我們會解釋兩個這種技術和它們的效果。

7.6.1. 1"共享內存"的多處理器系統

考慮示例7-5的生產者/消費者解決方案。儘管這個程序在現在的SPARC多處理器
系統上是可行的,但它假定了所有的多處理器系統都有高度有序的內存,因而是不
可移植的。

示例7-5 生產者/消費者問題--共享內存的多處理器

char buffer[size];
unsigned int in=0;
unsigned int out=0;
void producer(char item){
do
;/*nothing*/
while
(in - out == BSIZE);
buffer[in%BSIZE] = item;
in++;
}

char consumer(void){
char item;
do
;/*nothing*/
while
(in - out == 0);
item = buffer[out%BSIZE];
out ++;
}

如果這個程序僅有一個生產者和一個消費者,並且運行在一個共享內存的多
處理器系統當中,它似乎是正確的。In和out的差別是緩衝區中的產品數。生產者
等待直到有空位置,消費者等待直到緩衝區中有產品。
對於高度有序的內存(例如,一個處理器對內存的改變立刻對另一個處理器
生效),這種解決是正確的(即使考慮到in和out將最終溢出,程序仍然是正確的,
因為BSIZE比word型數據能表示的最大整數要小)。
共享內存的多處理器系統不一定擁有高度有序的內存。一個處理器對於內存
的改變未必會立刻通知其他處理器。如果一個處理器改變了兩處內存,其他處理
器不一定看到兩處改變按照預期的順序發生,因為對內存的改變不是立即執行的。
寫操作首先保存在CPU緩衝區里,對高速緩存是不可見的。處理器自己對這些緩衝
數據的維護是可靠的,但它對其它的處理器不可見,所以在數據寫到高速緩存之
前,其他處理器認為該寫操作沒有發生。
Solaris同步原語(見第三章)使用特殊的指令將數據從CPU緩衝區寫入高速
緩存。這樣,在共享數據的訪問前後加上鎖的保護,就可以保證內存的一致性。
如果內存的順序保護非常鬆散,在示例7-5中,消費者看到in變數被生產者
增加時,也許產品item還沒有放入產品緩衝區。這種情況被稱為weak ordering
(弱順序),因為一個處理器的操作在另一個處理器看來是打亂次序的(但內存對
同一個處理器總是保持一致)。解決這個問題的辦法是使用互斥鎖來強制更新高
速緩存。
現在的處理器趨向於"弱順序"。因此,程序員必須在操作全局或共享內存時
使用鎖。象在示例7-5和7-6中那樣,鎖是必不可少的。

7.6.1.2彼得森演算法(Peterson's Algorithm)

示例7-6是Peterson's Algorithm的一個實現,它控制兩個線程的互斥。這段
代碼試圖保證一個時間最多只有一個線程在執行關鍵代碼,進而當一個線程調用
mut_excl()時,它在很"近"的一個時刻進入該關鍵代碼。

假定線程進入關鍵代碼后很快退出。

示例7-6 兩個線程的互斥?

Void mut_excl(int me /*0 or 1*/){
Static int loser;
Static int interested[2]={0,0};
Int other;/* local variable */

Other = 1-me;
Interested[me]=1;
Loser=me;
While (loser == me && interested[other]);
/* critical section */
interested[me];
}
這個演算法在多處理器有高度有序的內存時,可能是可以正確運行的。
一些多處理器系統,包括一些SPARC系統,都有CPU緩衝區。如果一個線程發出
一條存儲指令,數據被放入CPU緩衝。這些數據最終都被送往高速緩存,但不是立刻
(注意高速緩存對其它的處理器來說是可見的,一致的,但數據並非立刻寫入高速
緩存)。
如果同時寫入多個內存地址,這些改變將按順序到達高速緩存和內存,但有一
定延遲。擁有這種屬性的SPARC多處理器系統被稱為全存儲順序
(TSO:Total Store Order)。
如果某個時間一個處理器向A地址寫,然後讀取B地址,而另一個處理器向B地址
寫,然後讀取A地址,其預期結果是或者第一個處理器得到B的新值,或者第二個處
理器得到B的新值,或者二者都得到,但不會發生兩個處理器都得到舊值的情形。
但是,由於從CPU緩衝區存取的延遲存在,這種不可能的事情就會發生。
在Peterson's Algorithm演算法中可能發生的是:兩個線程分別在兩個處理器上
運行,數據都保存在CPU緩衝區中,然後讀取另外一個,他們看到的都是舊的值(0),
表示另外的線程當前不在關鍵代碼中,所以他們共同進入關鍵代碼(注意,這種問
題在你測試的時候可能不會表示出來,但它是可能發生的)。
如果你用線程同步原語,就可以避免這個問題。同步原語強制地將CPU緩衝區
的數據寫入高速緩存。

7.6.1.3在共享內存的并行計算機里并行一個循環

在很多應用程序中,特別是數值計算,演算法的某些部分可以并行,而另外一
些部分必須順序執行(如示例7-7所示)

Code Example7-7多線程合作(Barrier同步)

While(a_great_many_iterations){
Sequential_computation
Parallel_computation
}

例如,你也許通過嚴格的線形計算獲得了一個矩陣,然後對這個矩陣進行並
行計算,再用運行結果創建另外一個矩陣,然後再進行并行計算,等等等等。
此類計算的并行演算法的特點是在計算時不需要太多同步,但使用結果時必須保
證結果都已得出。
如果執行并行計算的時間遠比創建和同步線程的時間長,那麼線程的創建和
同步不成問題。但是如果運算時間不是很長,線程的創建和同步時間就顯得非常重要。

7.2小結

這個編程指南包含了線程編程的基本注意事項。參看附錄A"示例應用程序",可
以看到很多討論過的特點和風格。

推薦讀物:
Algorithms for Mutual Exclusion by Michel Raynal (MIT Press, 1986)
Concurrent Programming by Alan Burns & Geoff Davies
(Addison-Wesley, 1993)
Distributed Algorithms and Protocols by Michel Raynal (Wiley, 1988)
Operating System Concepts by Silberschatz, Peterson, & Galvin
(Addison-Wesley, 1991)
Principles of Concurrent Programming by M. Ben-Ari (Prentice-Hall, 1982)



[火星人 ] Solaris2.4 多線程編程指南7--編程指南已經有551次圍觀

http://coctec.com/docs/program/show-post-72061.html