十年老司機詳解Linux多線程技術上篇(含實例源碼,值得收藏)

Linux的線程實現

Linux系統下的多線程遵循POSIX線程接口,稱為pthread。編寫Linux下的多線程程序,需要使用頭文件pthread.h,連接時需要使用庫libpthread.a。Linux下pthread是通過系統調用clone()來實現的。clone()是Linux所特有的系統調用,它的使用方式類似於fork()。

線程創建

int pthread_create(pthread_t * restrict tidp,

const pthread_attr_t * restrict attr,

void *(* start_rm)(void *),

void *restrict arg );

函數說明:tidp參數是一個指向線程標識符的指針,當線程創建成功後,用來返回創建的線程ID;attr參數用於指定線程的屬性,NULL表示使用默認屬性;start_rtn參數為一個函數指針,指向線程創建後要調用的函數,這個函數也稱為線程函數;arg參數指向傳遞給線程函數的參數。

返回值:線程創建成功則返回0,發生錯誤時返回錯誤碼。

因為pthread不是Linux系統的庫,所以在進行編譯時要加上-lpthread,例如:

# gcc filename -lpthread

在代碼中獲得當前線程標識符的函數為:pthread_self()。

例子:

十年老司機詳解Linux多線程技術上篇(含實例源碼,值得收藏)

線程退出

void pthread_exit(void * rval_ptr);

函數說明:rval_ptr參數是線程結束時的返回值,可由其他函數如pthread_join()來獲取。

如果進程中任何一個線程調用exit()或_exit(),那麼整個進程都會終止。線程的正常退出方式有線程從線程函數中返回、線程可以被另一個線程終止以及線程自己調用pthread_exit()函數。

線程等待

在調用pthread_create()函數後,就會運行相關的線程函數了。pthread_join()是一個線程阻塞函數,調用後,則一直等待指定的線程結束才返回函數,被等待線程的資源就會被收回。

int pthread_join(pthread_t tid, void ** rval_ptr);

函數說明:阻塞調用函數,直到指定的線程終止。tid參數是等待退出的線程id;rval_ptr是用戶定義的指針,用來存儲被等待線程結束時的返回值(該參數不為NULL時)。

例子:

十年老司機詳解Linux多線程技術上篇(含實例源碼,值得收藏)

可以看出,pthread_exit(5)實際上就相當於return 5,也就是說,線程函數為run()函數,線程退出就是run()函數運行完。這時候就能明白pthread_join()的真正意義了。

線程函數運行結束是可以有返回值的,這個函數的返回值怎麼返回呢?可以通過return語句進行返回,也可以通過pthread_exit()函數進行返回。函數的這個返回值怎麼來接收呢?就通過pthread_join()函數來接受。

當然也可以選擇不接受該線程的返回值,只阻塞該線程:

pthread_join(tid, NULL);

線程清除

線程終止有兩種情況:正常終止和非正常終止。線程主動調用pthread_exit()或者從線程函數中return都將使線程正常退出,這是可預見的退出方式;非正常終止是線程在其他線程的干預下,或者由於自身運行錯誤(比如訪問非法地址)而退出,這種退出方式是不可預見的。

不論是可預見的線程終止還是異常終止,都會存在資源釋放的問題,如何保證線程終止時能順利地釋放自己所佔用的資源,是一個必須考慮和解決的問題。

從pthread_cleanup_push的調用點到pthread_cleanip_pop之間的程序段中的終止動作(包括調用pthread_exit()和異常終止,不包括return)都將執行pthread_cleanup_push()所指定的清理函數。

void pthread_cleanup_push(void (* rtn)(void *), void * arg);

函數說明:將清除函數壓入清除棧。rtn是清除函數,arg是清除函數的參數。

void pthread_cleanup_pop(int execute);

函數說明:將清除函數彈出清除棧。執行到pthread_cleanup_pop()時,參數execute決定是否在彈出清除函數的同時執行該函數,execute非0時,執行;execute為0時,不執行。

int pthread_cancel(pthread_t thread);

函數說明:取消線程,該函數在其他線程中調用,用來強行殺死指定的線程。

例子1:

十年老司機詳解Linux多線程技術上篇(含實例源碼,值得收藏)


十年老司機詳解Linux多線程技術上篇(含實例源碼,值得收藏)

程序運行結果為:

十年老司機詳解Linux多線程技術上篇(含實例源碼,值得收藏)

例子2:

十年老司機詳解Linux多線程技術上篇(含實例源碼,值得收藏)

也就是說,pthread_exit()用於本線程自己調用,pthread_cancel()用於本線程來終結其他線程。

同時這裡也區分一下線程返回的return和pthread_exit:

pthread_exit()用於線程退出,可以指定返回值,以便其他線程通過pthread_join()函數獲取該線程的返回值。return,是函數返回,不一定是線程函數哦! 只有線程函數中return,線程才會退出;

pthread_exit()、return都可以用pthread_join()來接收返回值的,也就是說,對於pthread_join()函數來說是沒有區別的;

pthread_cleanup_push()所指定的清理函數支持調用pthread_exit()退出線程和異常終止,不支持return;

pthread_exit()為直接殺死/退出當前進程,return則為退出當前函數,但是在g++編譯器中,main中的return會被自動優化成exit(),所以在主函數中使用return會退出該進程所有線程的運行;

return會調用局部對象的析構函數,而pthread_exit()不會(線程本來就不建議用pthread_exit()這類方法自殺的,正確的方法是釋放所申請的內存後return)。

線程函數傳遞及修改線程的屬性

線程函數參數傳遞

在函數pthread_create()中,arg參數會被傳遞到start_rnt線程函數中。其中,線程函數的形參為void *類型,該類型為任意類型的指針。所以任意一種類型都可以通過地址將數據傳送給線程函數中。

例子:

十年老司機詳解Linux多線程技術上篇(含實例源碼,值得收藏)

數組作實參時,傳入的是數組的首地址,即傳入多個相同類型數據的首地址;結構體作實參時,傳入的是結構體的地址,即傳入多個不同數據類型的結構地址。

也就是說,如果線程函數中需要傳入多個不同數據類型的參數,但是依照pthread_create()的定義,僅可以傳入void *的類型的數據,參數數量為一個。這個時候就需要將不同數據類型的參數封裝成一個結構體,將這個結構體的地址傳入。

例子:

十年老司機詳解Linux多線程技術上篇(含實例源碼,值得收藏)

需要注意一下,線程函數和普通的函數一樣,每調用一次,局部變量都會配分一次內存,並且各自之間互不干擾。

線程屬性

之前線程創建函數pthread_create()函數的第二個參數都設置為了NULL,也就是說,都是採用的默認的線程屬性。對於大多數的程序來說,使用默認屬性就夠了,但還是有必要來了解一下相關的屬性。

屬性結構為pthread_attr_t,屬性值不能直接設置,必須使用相關的函數進行操作,初始化函數為pthread_attr_init(),這個函數必須在pthread_create()函數調用之前調用。

屬性對象主要包括是否綁定、是否分離、堆棧地址、堆棧大小、優先級。默認的屬性為非綁定、非分離、默認1M的堆棧、與父進程同樣級別的優先級。

線程綁定屬性

關於綁定屬性,涉及到另外一個概念:輕進程(Light Weight Process,LWP)。輕進程可以理解為內核進程,它位於用戶層和內核層之間。系統對線程資源的分配和對線程的控制時通過輕進程來實現的,一個輕進程可以控制一個或多個線程。默認情況下,啟動多少輕進程、哪些輕進程來控制哪些線程是由系統來控制的,這種狀況即稱為非綁定。綁定狀況下,則顧名思義,即某個線程固定地綁在一個輕進程之上。被綁定的線程具有較高的響應速度,這是因為CPU時間片的調度是面向輕進程的,綁定的線程可以保證在需要的時候它總有一個輕進程可用。通過設置被綁定的輕進程的優先級和調度級可以使得綁定的線程滿足諸如實時反應之類的要求。

設置線程綁定狀態的函數為pthread_attr_setscope,函數原型為:

int pthread_attr_setscope(pthread_attr_t * tattr, int scope);

函數說明:tattr參數為指向屬性結構的指針,scope參數為綁定類型,通常有兩個取值PTHREAD_SCOPE_SYSTEM(綁定)、PTHREAD_SCOPE_PROCESS(非綁定)。

返回值,pthread_sttr_setscope()成功完成後會返回0,其他任何返回值都表示出現了錯誤。

例子:

十年老司機詳解Linux多線程技術上篇(含實例源碼,值得收藏)

線程分離屬性

線程的是否可結合狀態決定線程以什麼樣的方式來終止自己。在任何一個時間點上,線程是可結合的(或非分離的,joinable)或者是分離的(detached)。

可結合屬性:創建線程時,線程的默認屬性是可結合的, 如果一個可結合線程結束運行但沒有被pthread_join(),則它的狀態類似於進程中的Zombie(僵死),即它的存儲器資源(例如棧)是不釋放的,所以創建線程者應該調用pthread_join()來等待線程運結束,並得到線程的退出碼,回收其資源;

可分離屬性:通過調用pthread_detach()函數該線程的可結合屬性將被修改為可分離屬性。一個分離的線程是不能被其他線程回收或殺死的,它的存儲器資源在它終止時由系統自動釋放。

設置線程是否分離的函數為pthread_attr_setdatachstate(),其原型為:

int pthread_sttr_setdetachstate(pthread_sttr_t * tattr, int detachstate);

函數說明:tattr參數為指向屬性結構的指針,detachstate參數為分離類型,通常有兩個取值PTHREAD_CREATE_DETACHED(分離)、PTHREAD_CREATE_JOINABLE(非分離、結合)。

返回值,pthread_attr_setdatachstate()成功完成後會返回0,其他任何返回值都表示出現了錯誤。

例子:

十年老司機詳解Linux多線程技術上篇(含實例源碼,值得收藏)

注意,如果使用PTHREAD_CREATE_JOINABLE創建非分離線程(默認),則假設應用程序將等待線程完成。也就是說,在費線程終止後,必須要有一個線程用pthread_join()來等待它,否則就不會釋放線程的資源,這將會導致內存洩漏。無論是創建的分離線程還是非分離線程,在所有線程都退出之前,進程都不會退出。

這與進程的wait()函數類似。

線程優先級屬性

線程優先級存放在結構sched_param中,設置線程優先級的接口是pthread_attr_setschedparam(),它的完整定義是:

struct sched_param {

int sched_priority;

}

int pthread_attr_setschedparam(pthread_attr_t *attr, struct sched_param *param);

例子:

十年老司機詳解Linux多線程技術上篇(含實例源碼,值得收藏)

線程的互斥

線程間的互斥是為了避免對共享資源或臨界資源的同時使用,從而避免因此而產生的不可預料的後果。臨界資源一次只能被一個線程使用。線程互斥關係是由於對共享資源的競爭而產生的間接制約。

互斥鎖

假設各個線程向同一個文件順序寫入數據,最後得到的結果一定是災難性的。互斥鎖用來保證一段時間內只有一個線程在執行一段代碼,實現了對一個共享資源的訪問進行排隊等候。互斥鎖是通過互斥鎖變量來對訪問共享資源排隊訪問。

互斥量

互斥量是pthread_mutex_t類型的變量。互斥量有兩種狀態:lock(上鎖)、unlock(解鎖)。

當對一個互斥量加鎖後,其他任何試圖訪問互斥量的線程都會被堵塞,直到當前線程釋放互斥鎖上的鎖。如果釋放互斥量上的鎖後,有多個堵塞線程,這些線程只能按一定的順序得到互斥量的訪問權限,完成對共享資源的訪問後,要對互斥量進行解鎖,否則其他線程將一直處於阻塞狀態。

操作函數

pthread_mutex_t是鎖類型,用來定義互斥鎖。

互斥鎖的初始化

int pthread_mutex_init(pthread_mutex_t * restrict mutex, const pthread_mutexattr_t * restrict attr);

restrict,C語言中的一種類型限定符(Type Qualifiers),用於告訴編譯器,對象已經被指針所引用,不能通過除該指針外所有其他直接或間接的方式修改該對象的內容。

函數說明:mutex為互斥量,由pthread_mutex_init調用後填寫默認值;attr屬性通常默認為NULL。

上鎖

int pthread_mutex_lock(pthread_mutex_t * mutex);

函數說明:mutex為互斥量。

解鎖

int pthread_mutex_unlock(pthread_mutex_t * mutex);

函數說明:mutex為互斥量。

判斷是否上鎖

int pthread_mutex_trylock(pthread_mutex_t * mutex);

返回值:0表示已上鎖,非0表示未上鎖。

銷燬互斥鎖

int pthread_mutex_destory(pthread_mutex_t * mutex);

例子:

十年老司機詳解Linux多線程技術上篇(含實例源碼,值得收藏)

這裡的互斥量的用處就是在sleep(5)之間的時間內,不會切換到另一個線程的線程函數中,因為已經用互斥量鎖定了。

自旋鎖

自旋鎖是一種用於保護多線程共享資源的鎖,與一般互斥鎖不同之處在於:當自旋鎖嘗試獲取鎖時以忙等待的形式不斷地循環檢查鎖是否可用。在多CPU的環境中,對持有鎖較短的程序來說,使用自旋鎖代替一般的互斥鎖往往能夠提高程序的性能。

自旋鎖和互斥鎖的區別

從實現原理上來講,互斥鎖屬於sleep-waiting類型的鎖,而自旋鎖屬於busy-waiting類型的鎖。也就是說:pthread_mutex_lock()操作,如果沒有鎖成功的話就會調用system_wait()的系統調用並將當前線程加入該互斥鎖的等待隊列裡;而pthread_spin_lock()則可以理解為,在一個while(1)循環中用內嵌的彙編代碼實現的鎖操作(在linux內核中pthread_spin_lock()操作只需要兩條CPU指令,unlock()解鎖操作只用一條指令就可以完成)。

對於自旋鎖來說,它只需要消耗很少的資源來建立鎖;隨後當線程被阻塞時,它就會一直重複檢查看鎖是否可用了,也就是說當自旋鎖處於等待狀態時它會一直消耗CPU時間;

對於互斥鎖來說,與自旋鎖相比它需要消耗大量的系統資源來建立鎖;隨後當線程被阻塞時,線程的調度狀態被修改,並且線程被加入等待線程隊列;最後當鎖可用時,在獲取鎖之前,線程會被從等待隊列取出並更改其調度狀態;但是在線程被阻塞期間,它不消耗CPU資源。

因此自旋鎖和互斥鎖適用於不同的場景。自旋鎖適用於那些僅需要阻塞很短時間的場景,而互斥鎖適用於那些可能會阻塞很長時間的場景。

操作函數

pthread_spinlock_t是鎖類型,用來定義自旋鎖。

自旋鎖的初始化

int pthread_spin_init(pthread_spinlock_t *lock, int pshared);

自旋鎖的銷燬

int pthread_spin_destroy(pthread_spinlock_t *lock);

上鎖

int pthread_spin_lock(pthread_spinlock_t *lock);

判斷是否上鎖

int pthread_spin_trylock(pthread_spinlock_t *lock);

解鎖

int pthread_spin_unlock(pthread_spinlock_t *lock);

C++實現自旋鎖

C++11提供了對原子操作的支持,其中std::atomic是標準庫提供的一個原子類模板。

對於lock函數,需要CAS的原子操作,可以使用std::atomic類模板的成員函數compare_exchange_strong();

對於unlock函數,可以使用std::atomic類模板的成員函數store來以原子操作的方式將flag置false。


十年老司機詳解Linux多線程技術上篇(含實例源碼,值得收藏)

未完待續……

需要C/C++ Linux服務器開發學習資料私信“資料”(資料包括C/C++,Linux,golang技術,Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒體,CDN,P2P,K8S,Docker,TCP/IP,協程,DPDK,ffmpeg等),免費分享


分享到:


相關文章: