天天看點

Linux線程學習筆記(APUE11/12章)pthread筆記

pthread筆記

-v0.1 2021.3.14 Sherlock apue11,12

-v0.2 2021.4. 2 Sherlock 增加線程同步的鎖這一部分

-v0.3 2021.5. 2 Sherlock 增加3/4部分

  1. 線程的建立和退出

Linux系統下線程和程序的概念是比較模糊的。一般來說,線程是排程的機關,程序是資源

的機關。本質上來說,核心看到都是一個個線程,但是線程之間可以通過共享資源,互相

之間又劃分到不同的程序裡。

從核心視角上看各種不同的ID會比較清楚。之前的這篇文章對程序ID, 線程ID,程序組,

線程組,會話已經有了簡單描述:

https://blog.csdn.net/scarecrow_byr/article/details/50437626?spm=1001.2014.3001.5501

在一個程序的多個線程裡調用getpid,這個是一個c庫對getpid系統調用的封裝,是以這

個函數傳回調用線程的程序ID, 這個ID其實就是這個線程組的線程組ID,也是這個線程組

的主線程的核心pid,這個從kernel代碼kernel/sys.c getpid的系統調用可以看的很清楚。

使用gettid系統調用可以得到一個線程的對應核心pid。

而所謂pthread庫的pthread_self()得到的隻是pthread庫自定義的線程ID,這個東西和上

面核心裡真正的各種ID是完全不同的。

線程的建立用pthread_create, 用的系統調用是clone。我們考慮線程結束的方法,線程執

行完線程函數後自己會退出,這其中也包括線程函數自己調用pthread_exit把自己結束。

線程也可以被程序中的其他線程取消,取消一個線程使用的函數是:pthread_cancel(pthread_t tid)。

用strace跟蹤下,可以發現pthread_cancel中的系統調用是tgkill(tgid, tid, SIGRTMIN),

這個系統調用向線程組tgid裡的tid線程發送SIGRTMIN信号。

線程可以用pthread_jorn在阻塞等待相關線程結束,并且得到線程結束所帶的傳回值。

線程可以注冊退出的時候要調用的函數。使用pthread_cleanup_push, pthread_cleanup_pop。

注冊的函數線上程調用pthread_exit或者是線程被pthread_cancel的時候執行,線程正常

執行結束時不執行。用strace -f跟蹤程序中所有線程的系統調用可以發現,對應的系統調用

是set_robust_list。

  1. 線程同步的鎖

線程之間共享變量的時候需要加鎖,以pthread庫為例,我們有pthread_mutex, pthead_spinlock,

pthread讀寫鎖。此外我們還可以自己實作鎖,這個的好處是不受具體線程庫的限制,

不好的地方是,我們自己實作的鎖一定沒有pthread庫實作的性能高。如下的測試代碼中

比較了pthread mutex/spinlock,以及自己實作的spinlock的性能:

https://github.com/wangzhou/tests/blob/master/lock_test/test.c

apue這裡的例子11-5很好的展示了一個引用計數的實作方式。

一定要避免A-B/B-A這種交叉加鎖的情況,但是注意這裡隻是加鎖,減鎖的順序可以随意。

如果一定要出現交叉加鎖的情況,要做成如果第二把鎖沒有搶到(是以,第二把鎖要用try

鎖),那麼已經鎖上的第一把鎖也要放開,要留出一條通道把可能的死鎖情況放過去。過

一段時間再鎖上第一把鎖,再try第二把鎖。

11-6的代碼對于交叉加鎖的處理方式是,需要上第二把鎖的時候,直接放開第一把鎖,

然後先上第二把鎖,再上第一把鎖,由于這個時候有個時間的空隙,可能不滿足之前的

條件了,是以要再check下之前需要第二把鎖的條件。當然apue舉這個例子的目的這裡是

為了說明可以簡化加鎖,需要在複雜度和性能之間權衡。

  1. 條件變量和信号量

條件變量和信号量都是為了線程/程序之間同步用的,提供了線程/程序之間互相等待、

通知的機制。簡單寫一個測試:

https://github.com/wangzhou/tests/blob/master/pthread/thread.c

https://github.com/wangzhou/tests/blob/master/pthread/sem.c

在thread.c和sem.c裡都會出現同一個問題,如果不在enqueue_msg函數裡unlock和發信号

之間增加時延,會出現發送線程一直持有鎖,接收線程得不到鎖進而無法及時接收的情況。

測試發現,在unlock和發送信号(比如thread.c的pthread_mutex_unlock和pthread_cond_signal)

之間增加usleep(1)的時延就可以使如上的這種情況不出現。

  1. 線程控制

pthread庫還有很多控制接口,這些接口可以改變如上接口的語義,或者增減新的功能。

比如,每種鎖的初始化接口都可以控制這些鎖在嵌套加鎖、不對稱加鎖等的語義,遇到

這樣的情況,可以配置成容許或者是傳回錯誤值; 可以配置鎖線上程之間還是程序之間

共享; 可以配置線程棧的大小等; 可以申請線程的私有資料; 可以使得一個函數隻執行

一次。

簡單寫一個線程私有資料的測試:

https://github.com/wangzhou/tests/blob/master/pthread/priv.c

可以看到,pthread_key_t key1的create沒有和線程綁定。基本的邏輯是,pthread_key_create

可以在任意一個線程裡,pthread_setspecific和pthread_getspecific是線上程裡對應的,

線上程A裡set的資料,可以線上程A裡通過get拿到,但是在另外的一個線程B裡對全局的

key1使用pthread_getspecific隻能拿到NULL。apue裡的例子把pthread_key_create放到

了每個線程裡,為了隻調用一次pthread_key_create, 還介紹隻跑一個函數一次的接口:

int pthread_once(pthread_once_t *initflag, void (*initfn)(void)),可以想象這個

函數的實作是先上鎖,然後檢查initflag,initflag沒有置上已經跑過的flag就調用initfn,

如果已經跑過就不用跑了。這個隻跑一次的接口在某些庫的設計裡是很有用的,比如一個

庫需要在程序使用的時候初始化一次,這裡就可以用相似的設計或者直接用這個API。

關于信号可以參考如下link,線程和信号的細節内容需要另外描述。

https://blog.csdn.net/scarecrow_byr/article/details/97621432?spm=1001.2014.3001.5501

線程在fork系統調用上遇到的問題,我們自己寫一個獨立的應用的時候比較難遇到,因為

全局受自己的控制,我們隻要一開始fork程序,做好規劃就好。但是,當我們要寫一個庫,

這個庫可以被其他上層代碼調用,我們的庫裡又要起獨立的線程時,這個問題就會出來。

這個問題的本質是自己寫的庫裡向上層export出了全局的資源,比如,如果庫裡隻出函數

接口,就沒有問題,所有庫裡的資源都在函數的棧上,但是,如果庫裡有了全局資源,相當

于,我們的庫向調用程序裡增加了全局的資源,調用程序将需要考慮這些全局資源(當然庫

可以向上層的調用者提出訴求或者接口)。具體看,子程序會從父程序那裡繼承:

全局變量(子程序在沒有寫之前,如果去讀這個變量,依然得到的是父程序裡的值,如果拿

這個值去做判斷就有可能出錯)、各種鎖、信号量和條件變量。如果fork出來的程序不是

調用exec系列函數去執行一個新的程式,那麼子程序裡擁有和父程序一樣的鎖、信号量和

條件變量。可以通過pthread_atfork提前挂上fork時候的回調函數進行處理,即一定是在

prepare回調裡先擷取所有的鎖,在parent、child裡再釋放所有的鎖,注意這裡的擷取釋放

的操作和父程序裡的可能擷取鎖的行為做了互斥。

preaed/pwrite可以保證多線程對一個fd的操作是原子的。

繼續閱讀