linux 线程管理、同步机制等
學了那么多有關進程的東西,一個作業從一個進程開始,如果你需要執行其他的東西你可以添加一些進程,進程之間可以通信、同步、異步。似乎所有的事情都可以做了。
對的,進程是當初面向執行任務而開發出來的,每個進程代表著一個動作,你可以說一個進程組代表一個任務,或者一個會話代表一個任務,關鍵是你的任務就是在進程的執行與進程之間的交互中被完成。
但是,我們知道,進程在操作系統中被設計成獨立的個體,進程與進程之間有絕對的界限,他們的通信是需要通過內核的。這個耗費是蠻大的,并且,進程對資源的占有也時常是一個讓進程編程負擔的原因。
總的來說,如果你希望系統中并行的運行的一些任務,你就需要跑多個進程,而進程它跑動的前提是對資源的占有,它是一個獨立體,從某種意義上講,它的執行不依賴其他的東西(當然內核的支持肯定是要的)。這種設計風格,保證了進程的獨立性,但是也增加了進程的重量。其次,不管進程獨立風格怎么樣,進程之間的通信始終是需要的。這種通信需要內核的支持,這也是不夠理想的。
基于上面的問題,線程的概念被提出來了。
沒有線程概念之前,進程被看做是一個執行與資源的最小占有體。進程被看做執行的一個最小體,什么意思呢,一個進程就是用來服務一個請求的。多個請求需要創建多個進程來服務請求。關鍵是,如果這些請求是類似的、關聯非常高的,這個時候,每個進程對資源的占有,以及進程之間的通信就非常頻繁。進程顯然不適合這種模式。
線程,抽象的講就是將進程這個作為執行的個體拆分成若干個小執行體,這些小的執行體共享一個進程所占有的資源。
如果一個進程,是一個手術室,那么該手術室需要對很多工具進行獨占,并且保持一定的獨立性(不獨立將會很危險,想想,不同的手術室,經常為某個手術工具而等待,將會很恐怖)。手術間的通信也會經過外面,而不是直接的。
而一個手術室內部呢?有若干個醫生、護士,他們各司其職,工具共享、交流通暢。這就是進程和線程的區別。
不過從另外一個角度上講,線程和進程只是從不同的角度(粗細度)看操作系統的執行體。就線程本身而言,它的最大特點就是共享一個進程的幾乎所有資源(內存空間等),但是作為一個執行體,它也擁有與進程類似的相關控制管理操作(創建、退出、等待、通信)。所以,認清進程與線程的區別的基礎上,看到他們的相同點,是學習兩者的好方法。
線程基本管理
創建線程
/usr/include/bits/pthread.h
Extern int pthread_create( pthread_t * __restrict??__new thread,
????????????????????????????????__constpthread_attr_t*??__restrict??__attr,
????????????????????????????????Void * (* __start _routine ) (void *),
????????????????????????????????Void *??__restrict __arg)
/usr/include/bits/pthreadtypes.h
Typedef unsigned long int pthread_t;
可以看到,創建一個新的線程,需要注入一段“執行腳本”,就像創建一個新的進程一樣,復制父進程的“執行腳本”,或者使用exec函數從文件系統中調入一個新的執行文件進來。進程是獨立體,自然“執行腳本”也是獨立體。而線程被定義為進程執行體的一部分,所以,線程的“執行腳本”自然是進程“執行腳本”的一個部分。
可知,上述所說的執行腳本,在編寫程序的時候,就是程序本身,程序內部最能代表一個相對獨立的執行體的東西,自然是函數。所以,新建一個線程的所需要的東西就是指定一個該進程(code)內部的函數作為該線程的執行腳本。從這點我們也可以看出,如果沒有線程的概念,進程就像是一個按照“執行腳本”(code)順序執行的執行體,線程的作用是從這個執行體中摳出一個相對獨立的部分來執行。有一定的異步性。從上層來講,一個進程仍然是按照用戶編程的程序進行跑著。但是線程似乎讓這個“執行體”并發的進行起來。
繼續使用手術室的例子,手術的過程就是一段手術的可執行程序中的code所指示的。每個線程,取該code中的某個部分做該做的事。主刀者做它的事,輸血者做輸血的事,觀察者看儀器。他們各司其職、但也并是不保持絕對的獨立。
可以說,添加了線程概念的進程,在執行的時候,有一定的異步特點。
?
線程退出
與進程的exit和abort對應。一個線程在該線程內部突出的方式有:
/usr/include/bits/pthreadtypes.h
Extern void pthread_exit (void * _retval);
線程退出的情景有:
1)?調用pthread_exit
2)?調用pthread_cancel
3)?所屬進程退出
4)?線程函數結束
5)?其中的一個線程執行exec函數
要知道,exec函數的意思是喚起一個新的可執行程序,來替換現有進程,這是一個進程級別的命令的,但是,在某個線程中執行,所以一個線程執行這個命令會導致其它的所有線程全部被退出。
等待線程
進程中,父進程可以使用wait和waitpid等待子進程的執行,線程中也是一樣:
Extern int pthread_join (pthread_t??__th, void??**??__thread_return);
獨立線程
使用等待線程,必須指定某個線程的id,必須是關聯的線程(自然是屬于一個進程的),一個進程內部的線程默認是與進程關聯的,可以通過函數執行,讓該進程不關聯。
Extern int pthread_detach (pthread_t??__th);
?
線程獨占的資源
這里我們講的線程獨占資源,是指那些線程退出的時候會釋放的資源,其他的線程是無法訪問的線程。其實這個問題我們只要知道,線程是進程中某個函數的異步執行而已,就很容易推到出來。
一個進程的內存空間中,有code區、數據區、BCC區,這三個區是可執行程序在還未進入內存的時候就已經分配好了的,所以,它是一個進程的固有區,進入內存后,再加上棧區和堆區。線程中有可能申請了全局或者靜態數據,這些數據都會在數據區或者BCC區中,線程中同時會有可能申請堆上空間,這個在一個進程內部,是通用的,因為該區的數據只受申請很釋放函數管理,與是哪個線程申請,哪個釋放無關。在一個進程內部它還真的無所謂被誰申請和釋放。
所以在一個進程內部,線程所獨占的,也就是局部變量,其實一個函數的角度去看,局部變量的確只屬于該函數,程序的其它的部分根本不知道它的存在。
如果沒有線程概念的時候,這些局部變量被壓入在統一的棧中,但是,有了線程,這種方式有不通用了,我們知道“執行腳本”執行的最根本依靠就是“棧的”先入后出方式。不同的線程之間是異步的,所以他們不可能在共享一個棧(如果是同步的,仍然是可以的)。
解決的方法可以想到,在沒有線程的概念中,所以的過程都在棧中發生,一個函數的過程局部變量在棧的某個部分相對其他事物是獨立了,也就是說,其他部分僅會依賴你的開始和結束,中間部分他們看不到。所以這個就可以獨立出來。
所以線程是可以獨立擁有自己的棧的。當然線程除了擁有自己獨立的棧,還擁有其他獨立的東西。
線程退出前的動作
與對于資源的占有,本該屬于進程的動作,進程退出它所占有的所有資源都會被釋放,所以進程在退出的時候無需太多考慮資源的釋放問題(可能,有什么對某個資源的不同進程之間的競爭,需要進程內部協調)。
但是在一個進程內部,線程之間也會出現資源的競爭,這不要與線程共享進程所占有的資源這個概念搞混淆。從另外一個角度上講,各個進程可以看做對系統擁有的所有資源共享呢。共享是共享,但是真正要使用的時候,有些工具就只能一個執行體使用。所以進程內部同樣存在資源競爭問題。只是范圍縮小了而已。
在進程中,由于各個線程處于一個“空間內”,有關資源競爭的控制直接由線程來做,我們設想,有一個資源記錄薄,上面記錄的資源全部只能是線程獨的。那個線程想使用某個工具就需要到該記錄簿上,打個記號,(如果已經被人使用了那只能等了)。因為線程對進程空間各項資源的共享,所以這種模式實現起來很方便。關鍵是,如果某個線程死掉了,但是沒有在死之前到記錄薄那里將那個占有記號給取消掉,那么其他的線程永遠也無法使用該工具。
所以線程,在可預見性退出或者不可預見性退出的之前需要從分考慮資源釋放問題。
當然,除了資源釋放,我們還有很多其他的工作,需要在退出之前做的。這個動作類似于進程的atexit定義的動作。在線程中,我們將那些要求在線程退出之前的動作成為清理工作。
Void??Pthread_cleanup_push( void (* routine)??(void *),??void??*arg);
Void??pthread_cleanup_pop (int execute);
在線程中使用這兩個函數來做有關的清理工作。
?
這中機制設計起來非常巧妙,在線程函數中,你什么時候申請了一個獨占資源,你都使用Pthread_cleanup_push函數注冊一個清理函數,該函數被壓入棧中。也就是說,你申請了多少資源(或者其他的事情),你希望在該線程結束之前被釋放(或者其他動作)。有可能有許多的動作被壓入棧中。
在結束之前,通過pthread_cleanup_push逐個出棧,并執行之前設定好的函數,來進行清理工作。
?
之所以選擇棧這個先入后出的結構,大家想想就明白了。就像為什么析構函數是與構造函數反方向釋放空間一樣。在申請資源的時候,只可能后面的資源(變量),依賴前面的資源。所以釋放的時候按照反的方向釋放才是安全的。
?
這兩個函數的具體用法,就是在線程函數中,在需要注冊清理函數的地方調用pthread_cleanup_push函數。在最后的結束的地方,逐個使用pthread_cleanup_push彈出函數執行。程序可能執行不到pthread_cleanup_push那么地方,但是系統規定,只要線程終止(不管是正常終止,還被強行終止),都會執行這個東西。
?
這個時候你就會發現這種清理機制的巧妙之處了。線程不管執行到那個地方(可能還有些pthread_cleanup_push函數都沒有執行),都可以終止,這個時候調用那些清理函數,就是合理的。如果使用那種類似于atexit的方式,寫死了在線程結束前要做的事情,那么有時候我資源都沒有申請(沒有執行到那個pthread_cleanup_push那個地方就被終止了),你卻要調用函數來釋放,是不合適的。而線程使用這種方式,保證了申請了才被釋放的機制。
?
取消線程
線程外部發出取消的命令,是以信號的形式發送給該線程的,也就是說并不是那種直接控制的方式,因為線程已經是一個獨立的執行體,所以它有權地決定是否以及如何聽取你的取消命令。
Extern int thread_cancel??(pthread_t??__cancelthread);
Extern int pthread_setcancelstate(int __state, int *??__oldstate);
連個state值,分別是:
PTHREAD_CANCEL_DISABLE
PTHREAD_CANCEL_ENABLE
Extern int pthread_setcanceltype(int __type, int *??__oldtype);
兩個type的有效值為:
PTHREAD_CANCEL_ASYNCHRONOUS
PTHREAD_CANCEL_DEFERRED
?
?
線程私有數據(tsd)
有時候,我們希望執行體之間共享、共用某個數據,通過兩個執行體對一個變量進行修改來達到通信的效果。(其實有一種方式在執行體之間傳遞數據就是使用參數,但是要實現兩個執行體之間的這種通信效果是很不方便的)
最好的方法就是使用全局變量。
當然全局變量是一種相對的概念,只要兩個執行體都認識該全局變量,應該就算是全局變量。
有了這個全局變量,我能就可以在不同的函數中實現通信,尤其是在這些函數已經獨立出來(使用線程)。
例如,對進程占有的某個資源的占用,我們使用一個全局變量來指示還有多少可以用的,每次新建一個線程,可以實施對該資源變量的修改來表達它的數量的增減。
對,這種進程線程共享機制是很有用的。
?
但是,有時候,我希望有些東西獨屬于某個線程,并且可以跨越線程的幾個函數。什么意思呢?進程中的code可以有很多的函數,而線程是從某個函數進入的,并且這個函數有可能訪問其他的函數。現在對于同一個函數fun1,它內部調用了fun2.同時兩個函數都操作了一個全局變量n。現在我從fun1函數這里開始產生出多個線程,每個線程都對這個變量n獨有,這就是說這個線程對變量n的更改只限于該線程內部的那些函數,與其他線程無關。這就與全局變量不同,全局變量沒有線程的差別,只要被誰改了,就被改了。
也許你會想到一個方法,使用參數值傳遞,將全局變量從fun1傳遞進入,但是這樣會很不方便,因為在某個一個線程中,可能有很多的函數都會使用該參數,這就是回到了前面我們討論的如何使用一個變量在各個執行體之間流轉的問題。使用的就是全局變量。
于是我們現在需要的就是一個類似于全局變量,但是有不是全局變量的東西,它屬于一個線程內部的全局變量。
這就是我們要學到的東西——線程私有數據(TSD)
Pthread_key_t??key;
在編寫代碼時使用這樣一個語句在程序中定義一個類似于全局變量的東西。
在每個函數內部使用函數:
Int pthread_key_create(pthread_key_t??* key, void ( *destr_function ) (void *));
這句話的作用就是,在某個線程中,執行這個函數,將key這個全局變量注入數據,這個時候,你要注意這不是普通的在線程內部
Key=10;
這個函數,會在該線程的范圍內,開閉一個獨立的空間來保存key的內容。在該線程的范圍內,該名字(key)所引用的地址都是那個地方。
不管哪個線程調用這個函數,都會獨立在線程范圍內開辟那個空間。名字使用key。
?
有些人會說,那這樣與我在該函數內部從新定義一個key,然后使用有什么區別呢?有的,由于這個可以仍然扮演全局變量的角色。
當我們編寫代碼的時候,我們并不區分線程,我們也不知道某個函數會被喚起多少個線程,但是不同的函數之間通暢的交流使用全局變量就是一個很好的方法,又由于全局變量不能保證線程之間的獨立。而TSD,在編碼的時候保持著全局變量的樣子和作用。在實際跑的時候,卻是一個與線程相關的全局變量。
?
當然了,Pthread_key_t是一個特殊的全局變量。
讀寫那個線程全局變量的需要使用特殊的函數。
Int pthread_setspecific (pthread_key_t key, const void * pointer);
Void * pthread_getspecific(pthread_key _t key);
所以,有關線程私有數據本來就是一個比較復雜的問題,它所表達的就是一個同名不同地址的全局變量。也就是全局同名,各線程中不同地址。
其實這個東西還是滿難理解的。如果是一個應用的技巧,也就是說不涉及的內核的特殊支持。那么可以這樣理解。
首先,定義個全局變量pthread_key_t??key?這樣的結構。
該結構自然可以被然后一個線程使用,該結構內部有一個這樣的列表。
List——>pointer>
其中,tid是當前線程的線程id,pointer是某個地址為void *?類型。
使用Pthread_key_create()函數意在創建一個這樣的結構,開始的那個全局變量很可能是一個類似于空殼的東西,似乎就是一個指針而已。只用通過這個函數,才能從系統中得到一個這樣的結果,并且順便,注冊一個函數,用于每個線程在退出的時候釋放與key關聯的那個空間。
Pthread_setspecific()函數,將某個地址注冊到那個key結構中,使用tid作為標示。
Pthread_getspecific(),將key結構中當前tid關聯的指針地址返回。
內存空間如下:
?
在某個線程中,只需要通過set/getsepcific函數,不管在那個函數內部,在一個線程內部從key那個結構得到的指針永遠是一致的。不過想來,這繞得也太大彎了。
線程屬性管理
前面介紹了線程基本的概念以及有關創建、退出、等待等等基本的內容,這一節主要介紹線程的屬性控制管理。
屬性是一件東西的內在內容,作為一個獨立的執行體,線程需要獨占一些東西以支持它的獨立執行。(雖然我們一直強調線程共享進程的資源)。
1)?程序計數器,由于線程是被拆分了的進程執行體,cpu中控制指令執行的東西必須被各個線程所獨有。這個事執行環境的其中一個。
2)?一組寄存器,與程序計數器一起構成了線程執行基本環境。這兩個屬性是程序員所無法控制,也不應該被控制,就應該被透明化的東西。
3)?棧,前面分析了,棧是一個程序執行的依賴結構,線程具有一定的獨立性,需要一個屬于自己的獨立棧來安排它的執行執行過程。這棧中的內容由用戶編寫的函數大體決定。
4)?線程信號掩碼,設置每個線程的阻塞信號。
5)?線程局部變量,存在于棧中
6)?線程私有數據(tsd),是一種線程級別的全局變量的應用(需內核支持)。
我們將線程相關的東西分為幾類,分別是,基本控制管理(創建、退出等等),內容基本管理(函數編寫時確定,包括線程是有數據),屬性管理(有關一個線程的棧大小、調度策略與參數、能否被等待等屬性,這些屬性影響著線程運行),以及程序員無法觸及的內容(程序計數器、寄存器等)。
POSIX給操作系統提供的管理線程屬性的結構體:
Typedefstruct??__pthread_attr_s
{
?????????Int???__detachstate;???//是否可被等待默認PTHREAD_CREATE_JOINABLE
?????????Int??__schedpolicy;???//調用策略默認?SHED_OTHRE.
?????????struct??__sched_param???__schedparm;??//調用策略參數默認為優先級0
?????????int??__inheritsched;??//是否繼承創建者的調度策略
?????????int???__scope;???//爭用范圍默認PTHREAD_SCOPE_PROCESS
?????????siee_t???__guardsize;???//棧保護區大小
?????????int????__stackaddr_set;???//
?????????void *??__stackaddr;??//棧起始地址,默認為NULL,系統自行分配
?????????size_t??__stacksize;??//棧大小,默認為0,系統默認棧大小
}pthread_attr_t;
上面是系統POSIX提供給我們管理線程屬性的對象結構,按理說我們能夠直接通過賦值控制它,但是,POSIX提供了一系列的操作函數來控制它。原因很有多的,關鍵是可以通過操作函數的控制來控制屬性設置的合理性。
注意,線程屬性對象與線程有一定的獨立性,這個結構本身并不屬于任何線程,只是當我們用于創建新的線程的時候,調用的那個pthread_create函數,需要給予的一個屬性參數,如果給出的是NULL那就是使用系統默認的屬性參數。
所以一般的過程是,在程序開始創建線程之前,系統先定義好這個線程屬性對象。主要的函數有:
(這些函數,有一個固定的格式,函數名以pthread_attr_開頭,返回值,大部分尊崇UNIX的管理int類型,0表示成功,-1表示失敗,不管是設置還是要獲取,都在函數參數列表中表現,第一個參數往往是一個線程參數對象的引用(指針),第二個參數是要設置的值(使用值傳遞),或者是要獲取(注入的屬性值),使用引用(指針傳遞),?并且,通常復雜參數也使用引用(指針),以防止復制該參數)
| 函數原型 | 說明 |
| Extern int??pthread_attr_init( pthread_attr_t * __attr) | 按照系統默認值初始化該線程屬性結構 |
| Extern int pthread_attr_destroy( pthread_attr_t * attr) | 銷毀已初始化的 |
| Extern int pthread_attr_setdetachstate( pthread_attr_t??*??__attr,??int??__detachstate) | 設置可被等待屬性 |
| Extern int pthread_attr_getdatachstate( Pthread_attr_t * __attr, int??*??__detachstate) | 獲得可被等待狀態 |
| Extern int??pthread_attr_setstacksize(pthread_attr_t * __attr, size_t??__stacksize) | 設置線程棧的大小 |
| Extern int pthread_attr_getstacksize(pthread_attr_t * __attr,size_t??*??__restrict??__stacksize) | 獲得線程棧的大小 |
| Extern int pthread_attr_setstackaddr(pthread_attr_t??* __attr, void *??__stackaddr) | 設置線程棧的起始地址(一般不能設置,默認值為NULL,表示讓系統決定,如果設置了就只能創建一個線程了,因為很難想象兩個線程共用一個棧,就像兩個進程共用一個棧那樣不合理) |
| Extern int pthread_attr_getstackaddr(pthread_attr_t * __attr, void ** __restrict??__stackaddr) | 獲得線程棧的起始地址 |
| Extern int pthread_attr_setguardsize(pthread_attr_t * __attr, size_t??__guardsize | 設置棧保護區大小(棧保護區設置需要多加考慮,不假思索的設置往往只會浪費內容空間) |
| Extern int pthread_attr_getguardisze(pthread_attr_t *?__attr, size_t??* __guardsize) | 獲取棧保護區大小 |
| Extern??int pthread_attr_setinheritsched(pthread_attr_t??* __attr, int??__inherit) | 設置是否從創建者那里繼承調度策略和關聯屬性。 PTHREAD_INHERIT_SCHED PTHREAD_EXPLICITY_SCHED(默認) |
| Extern int pthread_attr_getinheritsched(__constpthread_attr_t *?__restrict??__attr, int *??__inherit) ? | 獲取是否繼承調度策略。 |
| Extern??int pthread_attr_setschedpolicy( pthread_attr_t * __attr, int __policy) | 設置調度策略 #define SHCED_OTHER??0//默認 #define SHCED_FIFO???1 #define??SHCED_RR??2//時間輪轉 |
| Extern int pthread _attr_getschedpolicy(__constpthread_attr_t??* __restrict???__attr, int * __policy) | 獲取調度策略 |
| Extern int pthread_attr_setschedparam(pthread_attr_t *??__attr,??__conststructsched_param??*??_restrict??__param) | 設置調度參數 Structsched_param { Int???__sched_priority; } |
| Extern int pthread_attr_getschedparam(_constpthread_attr_t * __restrict??__attr,??structsched_param??*??__restrict | 獲取調度策略 |
| ? | ? |
?
上面列出了,通過管理線程屬性對象來在線程被創建時的管理該線程屬性。
在線程已經執行的時候,我們時候能夠在內容內部、或者是外部得到、設置相關屬性呢?當然是可以的。當然這些屬性有是在線程屬性對象中的,也有不在線程屬性對象中的,例如能否被取消等設置。
1)?Extern pthread_t pthread_self (void)?獲取當前線程id(線程內部)
2)?Extern int pthread_setschedprio(pthread_t??__target_thread,??int??__prio);(線程外部)
3)?Extern int pthread_setschedparam( pthread_t??__target_thread,??int??__policy,?structsched_param??*???__param);(線程外部)
4)?Extern int pthread_getschedparam(pthread_t??__target__thread, int??*__policy,?structsched_parm??*??_-param);?(線程外部)
5)?Extern int pthread_detach (pthread_t??__t);??(線程外部,改變線程是否能被等待)
6)?Extern int pthread_setcancelstate(int __state, int *??__oldstate);?(線程內部設置能否被取消)
7)?Extern int pthread_setcanceltype(int __type, int *??__oldtype);??(線程內部設置被取消的類型)
?
總的來說,設置獲取一個線程的相關屬性,可以通過線程屬性對象在創建的時候設置的方式、也可以直接對已存在的線程進行設置,兩者之間有交集。
線程間通信
作為一個執行體,線程間與進程間是一樣的,同樣需要通信、同步、異步。不過,線程間與進程間有許多的不同。線程共享一個進程的空間(當然自己也是保留獨有的東西的),我們前面分析過,線程就是將一個順序執行的可以執行文件,異步的執行起來,在一個進程空間中,線程遵守著進程code中的某個一段的規范,自行執行。
由于它們繼承資源的共享,所以,他們之間的通信可以很好的利用進程內部的共有事物進行相關的同步、異步。雖然有些時候仍然需要操作系統內核的支持,但是就是由于線程共享進程資源這個特點,是線程之間的通信非常的輕巧、方便。這是通常選擇多線程而不是多進程軟件方式的一個重要原因。
進程間通信的方法主要有:
·?????????互斥鎖:這是線程通信的基本原理和思想,保證同一時刻只能有一個線程訪問某個資源。
·?????????條件變量:配合互斥鎖,實現較好的資源互斥訪問策略。
·?????????讀寫鎖:更為豐富的互斥鎖機制,實現對資源的讀寫區別資源互斥訪問策略。
·?????????信號:進程信號在線程中的應用。
我們知道,當引入了線程的概念,就一個進程而言,它只是一個資源占有的實體而已,即使該進程只有一個線程,它也只是一個線程。當然對于進程間來說,是沒有線程的概念的。但是進程間的通信,又必須由進程內部某個線程來支持。這就是出現了一個很奇怪的現象。進程間是無線程的概念的,但是進程間的通信卻是有線程來完成的。(當然其實糾結這個問題基本毫無意義,一個手術室是一個進程,對外顯示為一個手術室,而不是若干個人。如果手術室中的人表示不同的線程,那么手術室之間的通信仍然是兩個手術室內部的人做的。只是對彼此而言,大家都認為是和手術室通信)。
信號
就信號這個概念來說,進程(當時沒有提出線程的概念的時候)可以給別人發信號(kill),可以給自己發信號(raise, alarm),來實現進程間、以及進程本身的異步執行。
引入線程概念之后,我們就知道,我們所的種種都是使用那個線程來完成的,所以說,那些有關進程的相關操作(例如信號)完全是在線程中可用的。例如,在一個進程中的如何線程里,你都可以使用kill函數向別的進程發出信號,你也可以使用raise給自己發出信號,在進程中的無論哪個線程安裝了該信號,就會執行。
引入線程后,在加入一些有關線程特有的信號操作,我們知道其實進程的信號操作,完全就是給線程用的,但是線程無關的。新加入的信號操作可以實現線程之間相互發信號,并且設置進程內部不同線程的信號掩碼。
這兩個操作都是針對線程來實現。
Extern int pthread_kill (pthread_t??_threadid, int??__signo);
Extern??intpthread_sigmask (int??__how,??__const??__sigset_t??*??__restrict??__newmask,
?????????????????????????????????????__sigest_t*??__restrict??__oldmask);
這個pthread_sigmask函數與pthread_sigprocmask函數對應,后者是來為整個進程進行屏蔽的,它的影響會到達每個線程,而前者則是為各個線程設置。
所以,信號,并不是獨屬于進程間通信的,它除了能夠表達在進程間進行信號的傳遞,它還能夠在線程間進行傳遞。至于其他的進程間通信機制,他們雖然仍然是通過線程操作、管理(引入線程后進程已經不再是一個執行體的代表了),但是仍然只是設計進程間的通信,無法將他們應用到線程間通信。
基于共有資源(變量)的線程間的同步、異步通信
序
我們總說,線程共享進程的資源,其實這里的共享,最大的部分還是對全局變量的共享。要理解,線程是某個可執行文件中的某個函數跳出來獨立執行而已,它并不脫離它所屬的環境,也就是說,要判斷某個線程是否對資源(變量、空間、資源等,其實在程序的層面上,資源就是變量或者是變量指向的某個實體)具有訪問和使用權限(或者說該資源對該線程是否可見),只要看程序就可以了,該線程所承載的函數能見到的資源,該線程就能夠訪問。
所以你會看到,線程間的通信手段(互斥鎖、條件變量、讀寫鎖)都是基于一個全局變量,不屬于任何函數的變量,是某個高層函數中的變量。這樣可以達到他們能夠看見的效果。
線程對進程資源的共享,表達出來的意思是“可見”,但是不同的線程是否能夠隨意的操作(讀寫)它可見的公共資源,就是線程間同步所要做的東西。
事實上,線程間數據通信還不是很重要,因為線程間共享率很高,不像進程間。但是這種共享率過高的情況導致了另外一個問題,那就是對某個資源的訪問,需要協調不同的線程的過程,要不然會出現混亂,也就是說需要管理線程對某個公共資源的操作,比如,同一時間只能有一個線程操作它(互斥鎖),或者是可以有很多線程同時讀,但只能有一個線程同時寫(讀寫鎖)。這就是同步,不要太糾結“同步”的同字,同步的意思是協調,規范不同執行的執行。
值得注意的是,在線程領域中,一方面,我們同步是為了線程之間能夠按照某種先后過程調度來訪問某個公共資源(變量);另一方面,我們實現這種同步的方式基本上也是使用公共資源(變量)的特性來實現的。
所以,線程中同步機制,起點和落點是線程的共享進程資源特性,我們使用那些不注重訪問策略的共享變量來控制,那些需要控制的共享變量。
互斥鎖
互斥鎖,是以排它的方式控制共享數據被線程訪問的過程,這種模式是通過控制線程中訪問該共享數據的代碼來控制他們的執行,而不是直接控制該共享數據。意思就是,線程中(函數中)的某一段代碼,必須要滿足某個條件才能繼續往下執行,否則阻塞在那里,直到條件達到位置。
它的效果類似于:
Boolmutex;
線程1:
While(!Mutex);
Mutext=false;
……
訪問公共資源代碼
……
Mutex=ture;
?
線程2:
與線程1類似。
兩個線程,使用公共數據mutex來控制對某個重要資源的互斥訪問,線程1中,一定要等到mutex的值為true時,才能突破while循環,進入下一步,否則會一直在那里執行著,直到等到線程二,執行完將mutex編程true。
這種模擬雖然意思到了,但是真正的linux互斥鎖,是阻塞策略。而不是循環問詢。自然是阻塞策略的好,當線程1發現mutex不可用,于是就阻塞起來,知道它可用的時候系統在發送信息喚醒該線程,并是該線程獲得該共享資源的控制權。這里關鍵的一個就是如何喚醒被阻塞的線程,這是需要內核支持的。當然為了程序員簡單,那些復雜的過程,都已經被包裝好了。主要的互斥鎖語句如下:
1)定義互斥鎖:
Pthread_mutex_t??lock;//必須是一個線程公共區(一般是一個全局變量)
2)初始化互斥鎖:
Extern??int pthread_mutex_init( pthread_mutext_t *??__mutex,??__constpthread_mutexattr_t *__mutexattr);
類似與使用線程屬性對象創建線程一樣,這里使用pthread_mutexattr_t線程互斥屬性的引用來初始化該互斥鎖,如果是NULL,則使用系統默認的方法。
還可以直接靜態初始化互斥鎖:
Pthread_mutex_tmp=PTHREAD_MUTEX_INITIALIZER;
這樣就免除了調用初始化函數,其中PTHREAD_MUTEX_INITIALIZER是這么被定義的:
#define PTHREAD_MUTEX_INITIALIZER??{ {0,?}},從這里我們依稀可以理解mutex這個結構的大體內容了。
注意:我們發現,在linux上的C編程中,總是有這樣的過程,先定義,然后初始化,我們怎么看都覺得怪怪的,那是因為,如果是面向對象語言,類似于結構體這樣的符合對象,是有構造函數的,而構造函數的作用就是分配并初始化符合結構。但是C中沒有,所以,一個復合結構被定義后需要初始化,而初始化,如果直接使用元素賦值的方法那將是非常恐怖和不便的,所以C中使用的是定義初始化方法,這個方法的存在意義就是作為構造函數而存在的,只不過需要程序員人為定義。
3)銷毀互斥鎖
Extern??int pthread_mutex_destroy ( pthread_mutex_t??*??__mutex)
這里有一個對應關系,在C++中,析構函數負責對對象生命周期的收尾工作,析構函數是在對象生命周期接收之前被調用的。
所以我們認為,構造函數或析構函數的功能是不應該包括分配空間和釋放空間,兩個函數只是在對象分配空間之后、以及釋放空間之前系統默認調用的函數。而C里面初始化函數和銷毀函數主要就是扮演著這個角色,所以這樣看兩者是對應的。這里的銷毀動作,也并不是釋放互斥鎖的空間,互斥鎖的空間(聲明周期)是由系統控制的,如果是全局變量,那么它全局存在,不會因為調用銷毀函數而空間被釋放了。不過可以這么理解,調用了銷毀函數,該互斥鎖會被清零,開始初始化以及后來的賦值的結果都被清零。
注意:這一點又在一次的證明了語言是沒有能力大小之分的。
4)申請互斥鎖
Extern??int pthread_mutex_lock (pthread_mutex * __mutex);
上面的阻塞式申請,意思是申請不到,當前線程就要阻塞,如下是一個非阻塞申請函數:
Extern int pthread_mutex_trylock(pthread_mutex??* __mutex);
非阻塞申請的意思是,申請一下,能申請就申請,不能申請我就走,不管了,干別的去。
5)釋放互斥鎖
Extern??int pthread_mutex_unlock (pthread_mutex_t *??_mutext);
?
條件變量
介紹了互斥鎖,感覺所有的需要在線程間進行排它訪問控制的事情,互斥鎖都能夠做到,但是互斥鎖仍然有些地方是無法做到的。
書上講了一個例子非常貼切,不過我們盡量尋找一個比較一般的場景。
線程A、B,需要排他訪問共享數據i,A會不斷的改變i的值,而B需要等到i為某個值的時候,才能觸發一定的動作。看起來似乎沒有問題。但是問題出現在,有可能i已經達到B需要的那個值,但是,當時A、B爭搶i的訪問權限的時候,A爭到了,并且對i進行了修改,于是后面B就算是再爭搶到了,也無法在執行指定的動作了。
有些人說,我可以建立兩個線程,做嚴格控制,讓每次A改變了之后,釋放該共享數據,一定要留一段時間,能夠讓B得到控制權。對似乎這個方法合適,但是當線程的復雜都增多,并發的線程增多時,你就會發現這種控制非常困難。
并且,即使通過比較好的時間控制,達到上面的效果,你會發現B執行它的動作只有一次機會(等到i為合適的值),但是就因為這個僅有的機會,B需要不斷的讀取釋放該數據,這樣明顯浪費了很多的寶貴資源。對于上面的這個場景,我們就希望,B不必要每次都去申請釋放該共享數據,我們希望,當i達到指定值的時候能夠,主動提醒B線程。
這就是條件變量。
這個“主動通知”的好處在哪里?關鍵是,B線程如果想要知道i是否達到指定值,就必須每次它被修改的時候,都要申請鎖,然后查看它,這個非常浪費的動作,A線程中如果在i到達了某個值的時候,能夠主動通知B線程是多么好的過程。這是條件變量的最大好處。
條件變量的使用場景:
多線程互斥訪問某個共享數據,但是,線程即使得到了該共享數據的訪問權,也需要先判斷是否達到訪問的條件,才進行相應的操作。所以條件變量是需要和互斥變量同時使用的,是在簡單排他訪問的基礎上,添加了條件的因素。
(1)??????定義、初始化、銷毀條件變量
Pthread_cond_tcondtion;
Extern??intpthread_cond_init??(pthread_cond_t??*??__restrict??__cond, __constpthread_condattr_t??* __restrict??__cond_attr);
Extern int pthread_cond_destroy (pthread_cond_t??* __cond);
(2)??????通知條件發生
Extern??intpthread_cond_signal (pthread_cond_t *??__cond);
Extern??intpthread_cond_broadcast(pthread_cond_t * __cond);
兩者的區別在于,signal只通知第一個等待該條件線程
(3)??????等待條件方法
Extern int pthread_cond_wait (pthread_cond_t * __restrict??__cond, pthread_mutex_t?*__restrict??__mutex);
extern int pthread_cond_timedwait (pthread_cond_t * __restrict??_cond, pthread_mutex_t?*__restrict??__mutex,??__conststructtimespec??* __restrict??__abstime);
兩者的區別在于,后者會在一定時間范圍內等待條件的發生。
并且,從等待條件方法的參數可以看出,條件變量在語法上已經是和互斥鎖緊密聯系著的。等待條件發生的那個線程,先申請互斥鎖,然后再使用其中一個函數申請條件,如果條件不符合,則阻塞,并且默認釋放該互斥鎖。等到從阻塞返回時,先申請到互斥鎖。
其實這里有一個很奇怪的事情,那就是為什么在使用等待條件的之前還要申請互斥鎖,既然等待條件方法對互斥鎖,有控制,那么大可以直接一句話完成。
與互斥鎖配合使用
1)?首先,定義、初始化、以及銷毀與互斥鎖是一樣的。
2)?在A線程,抓住一次互斥鎖后,先對共享變量進行操作,然后判斷是否出現特定條件。如果出現了,則首先釋放互斥鎖,然后通知條件發生。這里要注意,一定要先釋放鎖,因為,B線程一般就阻塞在等到條件的那個地方,這個地方要喚醒,首先需要申請關聯互斥鎖。如果沒有出現,則照常釋放。
3)?在B線程中,首先申請互斥鎖,然后,等待條件。
讀寫鎖
最后我們來看看讀寫鎖,讀寫鎖是對互斥鎖基本思想的一個擴充,這種擴充是面向計算機的一個特定策略哲學(可能不只是計算機)。就是一個共享數據,可以同時供多個執行體讀訪問,但只能同時供一個執行體寫訪問。這是就是我們通常所說的讀寫鎖。讀寫鎖與互斥鎖都稱為鎖,大家的基本思想都是一樣的。條件變量基本上,可以稱為互斥鎖上的一次擴展而已。
1)?定義、初始化、銷毀讀寫鎖
Pthread_rwlock_trwl;
Extern int pthread_rwlock_init (pthread_rwlock_t??*??__restrict??__rwlock, __constpthread_rwlockattr_t * __restrict??_attr);
Extern int pthread_rwlock_destroy (pthread_rwlock_t*??__rwlock);
2)?申請讀鎖
Extern int??pthread_rwlock_rdlock( pthread_rwlock_t * __rwlock);
Extern int pthread_rwlock_tryrdlock(pthread_rwlock_t * __rwlock);
3)?申請寫鎖
Extern int pthread_rwlock_wrlock(pthread_rwlock_t * __rwlock);
Extern int pthread_rwlock_trywrlock(pthread_rwlock_t * __rwlock);
4)?解鎖
Extern int pthread_rwlock_unlock(pthread_rwlock_t *??__rwlock);
?
?
總結
以上是生活随笔為你收集整理的linux 线程管理、同步机制等的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: linux查看和关闭后台执行程序
- 下一篇: 线程同步机制:互斥量、信号量、读写锁、条