《POSIX多線程程序設計》讀書筆記


一.      概述

1.    一個UNIX進程可以理解為一個線程加上地址空間、文件描述符和其他數據;

2.    多個線程可以共享一個地址空間,而做不同的事情。在多處理器系統中,一個進程中的多個線程可以同時做不同的工作;

3.    從某種成都上講,線程只是構造異步應用程序的另一種方式,但是它卻比其他用來構造異步程序程序的模型更有優勢;

4.    線程安全是指代碼能夠被多個線程調用兒不會產生災難性結果。它不要求代碼在多個線程中高效地運行。大部分現行函數可以利用PThreads提供的工具------互斥量、條件變了和線程私有數據,實現線程的安全。不需要保存永久狀態的函數。可以通過整個函數調用的串行化來實現線程安全。比如,只要在進入函數時加鎖,在退出函數時解鎖。這樣的函數可以被多個線程調用,但一次只能有一個線程調用它。

更有效的方式是將線程安全函數分為多個小的臨界區。這樣就允許多個線程進入該函數,雖然不能同時進入一個臨界區。更好的方式是將代碼改造為對臨界區數據的保護而不是對臨界區代碼的保護,這樣就可以 令不會同時訪問相同臨界數據的線程完全並行地執行;

正確的方法是將互斥量和數據流相關聯,保護數據而不是代碼;

“可重入”優勢用來表示“有效的線程安全”,即通過采用必將函數或庫函數轉換成一系列區域更加復雜的方式使代碼成為線程安全的。通過引入互斥量和線程私有數據可以實現線程安全,但通常需要改變接口來使函數可重入。可重入的函數應該避免以來任何靜態數據,最好避免依賴線程間任何形式的同步;

通常,函數可以將狀態保存在環境結構中,由調用者來負責狀態數據的同步。例如,UNIX中的readdir函數依次返回隊列中的每一個目錄項。為了讓readdir函數是線程安全的。你可以添加一個互斥量,每次當readdir被調用時,鎖住該變量,在函數返回時解鎖。另一種辦法,也是Pthreads對其readdir_r函數的做法,是避免函數內部的任何鎖操作,而是讓調用者在搜索目錄時分配一個數據結構來保存readdir_r的環境;

5.    當一個線程要進入一個函數但是需要等待別的線程釋放鎖,等待結束進入該函數的結果和未發生過等待所產生的結果一致,那這個函數就是可重入的,可重入異地線程安全,但線程安全不一定可重入;

6.    當你使用管道(pipe)將一個命令的輸出重定位到另一個命令的輸入是,就是啟動了幾個獨立程序,他們彼此通過管道來同步;

7.    當在腳本環境中輸入ls|more,將ls命令的輸出重定向到more命令的輸入時,實際上是在通過設定數據依賴性來描述命令的同步;

8.    線程就是進程里足以執行代碼的部分,在大多數計算機系統中,這意味着線程應包括以下內容:當前指令位置指針(通常稱為計數器或PC)、棧頂指針(SP)、通用寄存器、浮點或者地址寄存器。線程可能還包括像處理器狀態和協處理器寄存器等數據。線程不包括進程中的其他數據,如地址空間和文件描述符。一個進程中的所有線程共享文件和內存空間,包括程序文本段和數據段;

9.    當系統在進程間切換時,與進程相關的輸油硬件狀態都會失效。有些可能隨環境切換過程而改變,如數據緩存和虛擬內存轉換入口可能需要刷新;每一個進程有獨立的虛擬內存空間,但是同一進程中的線程卻共享相同的地址空間和其他進程數據;

10.在Digital UNIX平台上,用標志CFLAGS = -pthread –stdl –wl來編譯代碼,在Solaris系統中,用標志CFLAGS=-D_FEENTRANT –D_POSIX_C_ SOURCE= 199506 –lpthread來編譯;

11.pthread_create 函數建立一個線程,運行並由第三個參數(alarm_thread)指定的例程,並返回線程標識符ID(保存在thread引用的變量中);

pthread_detach 函數允許pthreads當前線程終止時立刻回收線程資源;

pthread_exit 函數終止調用線程;

12.barrier是一種簡單的同步機制,它阻塞每個線程直到到達某個“限額”類,然后所有線程被解除阻塞。例如,可以使用barrier機制來確保:只有當所有線程都准備好了才執行一段並行代碼;

13.線程系統的三個幾倍要素:執行環境、調度和同步;

二.      線程

1.    為建立線程,你需要在程序中聲明一個pthread_t類型的變量。如果只需要在某個函數中使用線程ID,或者函數直到線程終止時才返回,則可以將線程ID聲明為自動存儲類型。不過大部分時間內,線程ID保存在共享(靜態或者外部)變量中,或者保存在堆空間的結構體中。

2.    線程可以通過調用pthread_self來獲得自身的ID;

3.    如果需要知道線程何時結束,就需要保存線程ID;

4.    Pthreads提供了pthread_equal函數來比較兩個線程ID,只能比較二者是否相同;

5.    初始線程(主線程)是特殊的;

6.    分離一個正在運行的線程不會對線程帶來任何影響,僅僅是通過系統當該線程結束時,其所屬資源可以被回收;

7.    盡管“線程蒸發”有時有用,但大部分時間進程要比你創建的線程“長命”。為確保終止線程的資源對進程可用,應該在每個線程結束時分離它們。一個沒有被分離的線程終止時會保留其虛擬內存,包括他們的堆棧和其他系統資源。分離線程意味着通知系統不再需要此線程。允許系統將分配給它的資源回收;(避免內存泄露)

8.    pthread_join函數將阻塞其調用者直到制定線程終止,然后,可以選擇地保存線程的返回值。調用pthread_join函數自動分離指定的線程;

9.    當需要某個線程結束后我們才可以干某件事,就需要調用pthread_join函數;

10.程序調用pthread_create函數創建線程,然后調用pthread_join等待線程結束;(join連接的概念類似於讓被連接線程與當前發起join的線程(可能是主線程)發生連接,成為類似在主線程中同步運行的效果)

11.可以在main函數中調用pthread_exit,這樣進程就必須等到所有線程結束才能終止;

12.在少數情況下,多個線程需要知道某個特定線程何時結束,則這些線程應該等待某個條件變量而不是調用pthread_join函數。被等待的線程應該將其返回(或任何其他信息)保存在某個公共的位置,並將條件變量廣播給所有在其上等待的線程以喚醒它們;

13.就緒(ready):線程能夠允許,但是在等待可用的處理器。可能剛剛啟動,貨剛剛從阻塞中回復,或者被其他線程搶占;

運行(running):線程正在運行。在多處理器系統中,可能有多個線程處於運行太;

阻塞(blocked):線程由於等待處理器外的其他條件無法運行,如條件變量的改變、加鎖互斥量或I/O操作結束;

終止(terminated):線程從起始函數中返回,或調用pthread_exit,或者被取消,終止自己並完成所有資源清理工作。不是被分離,也不是被連接,一旦線程被分離或者連接,它就可以被回收;

14.如果線程已被分離,則他立刻被回收重用(這難道不是比“銷毀”線程好嗎?大部分系統可以重用資源來建立新線程);否則,線程停留在終止態直到被分離或者被連接;

15.有關線程創建最重要的是,在當前線程從函數pthread_create中返回以及新線程被調度執行之間不存在同步關系。即,新線程可能在當前線程從pthread_create返回之前就運行了,甚至在當前線程從pthread_create返回之前,新線程就可能已經運行完畢了;

16.當創建不需連接的線程時,應該使用detachstate屬性簡歷線程使其自動分離;

17.如果使用detachstate屬性(設為PTHREAD_CREATE_DETACH)建立線程或者調用pthread_detach分離線程,則當線程結束時將被立刻回收;

18.只有互斥量的主任能夠解鎖它。如果線程終止時還有加鎖的互斥量,則該互斥量就不能被再次使用(因為不會被解鎖);

三.      同步

1.    可以拷貝指向互斥量的指針,這樣就可以使多個函數或線程共享互斥量來實現同步;

2.    當不再需要一個通過pthread_mutex_init調用動態初始化的互斥量時,應該調用pthread_mutex_destroy來釋放它。不需要釋放一個使用PTHREAD_MUTEX_INITIALIZER宏靜態初始化的互斥量;

3.    當確信沒有線程在互斥量上阻塞時,可以立刻釋放它;

4.    當調用pthread_mutex_lock加鎖互斥量的時候,如果此時互斥量已經被鎖住,則調用線程將被阻塞;Pthreads提供了pthread_mutex_trylock函數,當地調用互斥量已被鎖住時候調用該函數將返回錯誤代碼EBUSY;

5.    避免死鎖的算法:

固定加鎖層次:所有需要同時加鎖互斥量A和互斥量B的代碼,必須首先加鎖互斥量A然后加鎖互斥量B;即,如果互斥量間不存在明顯的邏輯層次,則可以建立任意的固定加鎖層次。例如,你可以建立這樣一個加鎖互斥量集合的函數:將集合中的互斥量按照ID地址順序排列,並以此順序加鎖互斥量;某某種程度上講,只要總是保持住相同的順序,順序本身就並不真正重要;

試加鎖和回退:在鎖住某個集合中的第一個互斥量后,使用pthread_mutex_trylock來加鎖集合中的其他互斥量,如果失敗則將集合中所有已加鎖互斥量釋放,並重新加鎖;“回退”意味着你以正常的方式鎖住集合中的第一個互斥量,而調用pthread_mutex_trylock函數有條件地加鎖集合中其他互斥量。如果pthread_mutex_trylock返回EBUSY,則你必須釋放已經擁有的所有屬於該集合的互斥量並重新開始;

死鎖就是當你嘗試調用一個mutex的lock的時候,想要進行lock,有可能這個mutex已經被別的線程lock了,你就無法再去lock這個mutex,就必須等另外那個線程unlock這個mutex,兩個線程等對方鎖住的mutex,就死鎖了;mutex是系統級別的,所以線程彼此之間知道能否成功鎖住一個mutex;

比如代碼:

Mutex mutex_a, mutex_b;
void function1()
{
    pthread_mutex_lock(&mutex_a)         
}
void function2()
{
    pthread_mutex_lock(&mutex_b);
}
 
 //thread A callstack
 function1();
 function2(); 

 //thread B callstack
 function2();
 function1(); 

以上代碼中線程A和線程B如果同時走完了各自第一個函數,在准備走自己的第二個函數的時候就會造成死鎖;

6.    要小心使用鎖鏈。很容易寫出良妃處理器的代碼:代碼大部分時間再加鎖和解鎖時從來不會遇到競爭的互斥量。僅當多個線程幾乎總是活躍在層次中的不同部分時才應該使用鎖鏈;

7.    條件變量總是返回鎖住的互斥量;

8.    條件變量是與互斥量相關、也與互斥量保護的共享數據相關的信號機制。再一個條件變量上等待會導致以下原子操作:釋放相關互斥量,等待其他線程發給該條件變量的信號(喚醒一個等待者)或廣播該條件變量(喚醒所有等待者)。當等待條件變量時,互斥量必須始終鎖住;當線程從條件變量等待中醒來時,它重新繼續鎖住互斥量;

9.    條件變量就是允許使用隊列的線程之間交換隊列狀態信息的機制;

10.條件變量的作用是發信號,而不是互斥;

11.任何條件變量在特定時間只能與一個互斥量相關聯,而互斥量則可以同時與多個條件變量關聯;

12.當線程醒來時,再次測試謂詞同樣重要;

13.內存屏障(memory barrier)確保:所有在設置內存屏障之前發起的內存訪問,必須限於在設置屏障之后發起的內存訪問之前完成;

內存屏障是一堵移動的牆,而不是刷新cache的命令;

不像其他的內存訪問,內存控制器不能刪除內存屏障,不能越過它,直到完成在內存屏障之前的所有操作;

14.如果其中一個內存單元同時被另外一個處理器寫入,則將丟失一般數據。這杯成為“word tearing”;

四.      使用線程的幾種方式

1.    流水線:每個線程反復地在數據系列集上執行同一種操作,並把操作結果傳遞給下一步驟的其他線程,這就是“流水線”(assembly line)方式;

工作組:每個線程都在自己的數據上執行操作。工作組中的線程可能執行同樣的操作,也可能執行不同的操作,但是他們一定獨立運行;

客戶端/服務器:一個客戶為每一件工作與一個獨立的服務器“訂契約”。通常“訂契約”是匿名的-----一個請求通過某種接口提交;

2.    在工作組中,數據由一組線程分別獨立地處理。循環的“並行分解”通常就是屬於這種模式;

五.      線程高級編程

1.    Pthreads允許每個線程控制自己的結束,它能回復程序不變量並且解鎖互斥量,當線程完成一些重要的操作時,它甚至能推遲取消;

2.    默認情況下,取消被推遲執行,並且僅僅能在重新中特定的點發生;

3.    取消一個線程是異步的。即,當pthread_cancel調用返回時,線程未必已經被取消,可能僅僅通知有一個針對它的未解決的取消請求。如果需要知道線程在何時被實際終止,就必須在取消它之后調用pthread_join與它連接;

4.    線程能清理並終止自己,而不必擔心再次被取消;

5.    沒有方法“處理”取消並執行------線程必須或者被徹底推遲取消,或者終止;

6.    在獲得一個資源以后,並且在任何取消點之前,通過調用pthread_clearup_push生命一個清除處理函數。在釋放資源前,但是在任何取消點以后,通過調用pthread_creanup_pop刪除清理處理函數;

7.    下列函數在任何Pthreads系統上總是取消點: pthread_cond_wait   fsync    sigwaitinfo pthread_cond_timedwait mq_receive  sigsuspend pthread_join      mq_send     sigtimedwait pthread_testcancel   msync        sleep    sigwait nanosleep   system       aio_suspend      open   tcdrain close    pause   wait     creat    read     waitpid fcntl(F_SETLCKW)    sem_wait    write

8.    避免異步取消;很難正確使用異步取消,並且很少有用;除非你寫了可以安全地異步取消的代碼,否則在異步取消啟動時不要調用任何代碼,即使要這樣做,也要三思;

9.    可以把每個線程考慮為有一個活動的清除處理函數的棧。調用pthread_cleanup_push將清除處理函數加到棧中,調用pthread_cleanup_pop刪除最近增加的處理函數。當線程被取消時或當它調用pthread_exit退出時,pthreads從最近增加的清除處理函數開始,一次調用各個活動的清除處理函數。當所有的活動的清除處理函數返回時,線程被終止;

10.你不能在一個函數內壓入一個清除處理函數而在另外的函數中彈出它。pthread_cleanup_pop操作可能作為宏被定義,這樣pthread_cleanup_push中可能含塊開始的大括號“{”而pthread_creanup_pop中則包含了匹配的塊結束大括號“}”。當使用清除處理函數時,如果希望代碼可移植,你必須總是記住這一限制;

11.只要設置pthread_cleanup_pop的參數非零,則即使沒有發生取消,清除處理函數仍將被調用;

12.在進程內的所有線程都享有相同的地址空間,即意味着任何聲明為靜態或者外部的變量,或在進程堆生命的變量,都可以被進程內所有的線程讀寫;

13.如果每個線程都需要一個私有變量值,則必須在某處存儲所有的值(像UE4中多線程渲染就是好每個線程的回調函數都在一個類中,類成員變量存儲該線程所需要的值,回調函數都是static的)。

14.每個線程調用pthread_once保證線程私有數據鍵被創建;

15.當程序不在需要時,pthreads允許你調用pthread_key_delete釋放一個線程私有數據鍵;

16.可以使用pthread_getspecific 函數來獲得線程當前的鍵值,或調用pthread_setspecific 來改變當前的鍵值;

17.如果你的線程私有數據是堆存儲的地址,並且你想要在destructor函數中釋放存儲,就必須使用傳遞給destructor的參數,而非調用pthread_getspecific 的參數;

18.Destructor功能僅僅當線程終止時被調用,而不是當線程私有數據鍵的值改變時;

19.當你創建一個線程私有數據鍵時,pthreads允許你定義destructor函數。

20.當一個線程退出時,pthreads在進程中檢查所有的線程私有數據鍵,並且將所有不是空的線程私有數據鍵置為空,然后調用鍵的destructor函數;

21.標准要求pthreads實現在核對列表某個固定的次數后再放棄。當它放棄時,最終的線程私有數據值沒有被破壞。如果值是指向堆存儲器的指針,結果可能是一個內存泄露,因此一定要小心;

22.大多數操作系統至少在pthreads線程和處理器之間有一層附加的抽象,這就是我所說的核實體(因為那是pthreads使用的術語);

六.      POSIX針對線程的調整

1.    在一個forked的進程中,pthreads不會終止其他線程,好像他們調用pthread_exit推出了或是好像他們被取消。他們只是簡單地不再存在。即,線程不再運用線程私有數據destructors或者清除處理函數。如果子進程准備調用exec運行一個新程序,這不是一個問題,但是如果你使用fork克隆一個多線程程序,注意你可能失去對存儲器的存取,特別是僅僅存儲線程私有數據值的堆存儲器;

2.    應避免在線程的代碼中使用fork,除非子進程想很快地exec一個新程序;

3.    exec 函數並沒有因為引入線程而受到很多影響。exec函數的功能是取消當前程序的環境並且用一個新程序代替它;對exec的調用,將很快的充值進程內除了調用exec的線程外的所有線程。他們不執行清除處理器或線程私有數據destructors---線程只是簡單地停止存在;

4.    pthreads增加了pthread_exit函數,該函數能在進程繼續運行的同時導致多個單個線程的退出;

5.    線程不會執行清除處理器或線程私有數據destructors函數,調用exit具有同樣的效果;

6.    從主函數中調用pthread_exit將在不影響進程內其他線程的前提下終止起始線程,允許他們繼續和正常完成;

 

其他:

1. 當一個線程A利用mutex lock住一塊代碼的時候,線程B雖然可以存儲着這個mutex的指針,甚至可以在線程A鎖住這個mutext后線程B即解鎖它,但是最好不要這么做,也就是說,最好不要用其他線程解鎖當前線程鎖住的mutex;因為當線程B盲目解鎖這個mutex后,線程A仍在臨界區代碼中運行,其他線程(甚至也可能是B線程)就可以因為這個mutex已經被解鎖同時進入這塊臨界區代碼,這樣這個臨界區會出現兩個線程同時運行的情況,臨界區也就名存實亡了。 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM