多線程系列文章源碼頭文件內(nèi)容:
#include<pthreaad.h>
#include<unistd.h>
#include<stdio.h>
作為程序員颖榜,就是要減少重復(fù)勞動旨枯,拒絕一切無目的的DIY平挑。哪怕只有三行代碼客税,哈哈??
上一篇文章?最后我們提到了高效的同步機(jī)制艘策,本篇文章來個大亂燉蹈胡,每個同步機(jī)制后續(xù)會專門再分享。
互斥鎖
盡管在Posix Thread中同樣可以使用IPC的信號量機(jī)制來實(shí)現(xiàn)互斥鎖mutex功能,但顯然semphore的功能過于強(qiáng)大了审残,在Posix Thread中定義了另外一套專門用于線程同步的mutex函數(shù)梭域。
1. 創(chuàng)建和銷毀
有兩種方法創(chuàng)建互斥鎖,靜態(tài)方式和動態(tài)方式搅轿。POSIX定義了一個宏P(guān)THREAD_MUTEX_INITIALIZER來靜態(tài)初始化互斥鎖病涨,方法如下:?
pthread_mutex_t mutex=PTHREAD_MUTEX_INITIALIZER;?
在LinuxThreads實(shí)現(xiàn)中,pthread_mutex_t是一個結(jié)構(gòu)璧坟,而PTHREAD_MUTEX_INITIALIZER則是一個結(jié)構(gòu)常量既穆。
動態(tài)方式是采用pthread_mutex_init()函數(shù)來初始化互斥鎖,API定義如下:?
int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *mutexattr)?
其中mutexattr用于指定互斥鎖屬性(見下)雀鹃,如果為NULL則使用缺省屬性幻工。
pthread_mutex_destroy()用于注銷一個互斥鎖,API定義如下:?
int pthread_mutex_destroy(pthread_mutex_t *mutex)?
銷毀一個互斥鎖即意味著釋放它所占用的資源黎茎,且要求鎖當(dāng)前處于開放狀態(tài)囊颅。由于在Linux中,互斥鎖并不占用任何資源傅瞻,因此LinuxThreads中的pthread_mutex_destroy()除了檢查鎖狀態(tài)以外(鎖定狀態(tài)則返回EBUSY)沒有其他動作踢代。
2. 互斥鎖屬性
互斥鎖的屬性在創(chuàng)建鎖的時候指定,在LinuxThreads實(shí)現(xiàn)中僅有一個鎖類型屬性嗅骄,不同的鎖類型在試圖對一個已經(jīng)被鎖定的互斥鎖加鎖時表現(xiàn)不同胳挎。
當(dāng)前(glibc2.2.3,linuxthreads0.9)有四個值可供選擇:
PTHREAD_MUTEX_TIMED_NP,這是缺省值溺森,也就是普通鎖慕爬。當(dāng)一個線程加鎖以后,其余請求鎖的線程將形成一個等待隊列屏积,并在解鎖后按優(yōu)先級獲得鎖医窿。這種鎖策略保證了資源分配的公平性。
PTHREAD_MUTEX_RECURSIVE_NP炊林,嵌套鎖留搔,允許同一個線程對同一個鎖成功獲得多次,并通過多次unlock解鎖铛铁。如果是不同線程請求隔显,則在加鎖線程解鎖時重新競爭。
PTHREAD_MUTEX_ERRORCHECK_NP饵逐,檢錯鎖括眠,如果同一個線程請求同一個鎖,則返回EDEADLK倍权,否則與PTHREAD_MUTEX_TIMED_NP類型動作相同掷豺。這樣就保證當(dāng)不允許多次加鎖時不會出現(xiàn)最簡單情況下的死鎖捞烟。
PTHREAD_MUTEX_ADAPTIVE_NP,適應(yīng)鎖当船,動作最簡單的鎖類型题画,僅等待解鎖后重新競爭。
3. 鎖操作
鎖操作主要包括加鎖pthread_mutex_lock()德频、解鎖pthread_mutex_unlock()和測試加鎖pthread_mutex_trylock()三個苍息,不論哪種類型的鎖,都不可能被兩個不同的線程同時得到壹置,而必須等待解鎖竞思。對于普通鎖和適應(yīng)鎖類型,解鎖者可以是同進(jìn)程內(nèi)任何線程钞护;而檢錯鎖則必須由加鎖者解鎖才有效盖喷,否則返回EPERM;對于嵌套鎖难咕,文檔和實(shí)現(xiàn)要求必須由加鎖者解鎖课梳,但實(shí)驗(yàn)結(jié)果表明并沒有這種限制,這個不同目前還沒有得到解釋余佃。在同一進(jìn)程中的線程暮刃,如果加鎖后沒有解鎖,則任何其他線程都無法再獲得鎖咙冗。
int?pthread_mutex_lock(pthread_mutex_t?*mutex)??
int?pthread_mutex_unlock(pthread_mutex_t?*mutex)??
int?pthread_mutex_trylock(pthread_mutex_t?*mutex)?
pthread_mutex_trylock()語義與pthread_mutex_lock()類似沾歪,不同的是在鎖已經(jīng)被占據(jù)時返回EBUSY而不是讓出CPU漂彤,掛起等待雾消。
4. 其他
POSIX線程鎖機(jī)制的Linux實(shí)現(xiàn)都不是取消點(diǎn),因此挫望,延遲取消類型的線程不會因收到取消信號而離開加鎖等待立润。值得注意的是,如果線程在加鎖后解鎖前被取消媳板,鎖將永遠(yuǎn)保持鎖定狀態(tài)桑腮,因此如果在關(guān)鍵區(qū)段內(nèi)有取消點(diǎn)存在,或者設(shè)置了異步取消類型蛉幸,則必須在退出回調(diào)函數(shù)中解鎖破讨。
這個鎖機(jī)制同時也不是異步信號安全的,也就是說奕纫,不應(yīng)該在信號處理過程中使用互斥鎖提陶,否則容易造成死鎖。
條件變量
條件變量是利用線程間共享的全局變量進(jìn)行同步的一種機(jī)制匹层,主要包括兩個動作:一個線程等待"條件變量的條件成立"而掛起隙笆;另一個線程使"條件成立"(給出條件成立信號)。為了防止競爭,條件變量的使用總是和一個互斥鎖結(jié)合在一起撑柔。
1. 創(chuàng)建和注銷
條件變量和互斥鎖一樣瘸爽,都有靜態(tài)動態(tài)兩種創(chuàng)建方式,靜態(tài)方式使用PTHREAD_COND_INITIALIZER常量铅忿,如下:
pthread_cond_t?cond=PTHREAD_COND_INITIALIZER??
動態(tài)方式調(diào)用pthread_cond_init()函數(shù)剪决,API定義如下:?
int?pthread_cond_init(pthread_cond_t?*cond,?pthread_condattr_t?*cond_attr)??
盡管POSIX標(biāo)準(zhǔn)中為條件變量定義了屬性,但在LinuxThreads中沒有實(shí)現(xiàn)辆沦,因此cond_attr值通常為NULL昼捍,且被忽略。
注銷一個條件變量需要調(diào)用pthread_cond_destroy()肢扯,只有在沒有線程在該條件變量上等待的時候才能注銷這個條件變量妒茬,否則返回EBUSY。因?yàn)長inux實(shí)現(xiàn)的條件變量沒有分配什么資源蔚晨,所以注銷動作只包括檢查是否有等待線程乍钻。API定義如下:
int pthread_cond_destroy(pthread_cond_t *cond)
2. 條件等待和激發(fā)
int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex)
int pthread_cond_timedwait(pthread_cond_t *cond, pthread_mutex_t *mutex, const struct timespec *abstime)
等待條件有兩種方式:
無條件等待pthread_cond_wait()和計時等待pthread_cond_timedwait(),其中計時等待方式如果在給定時刻前條件沒有滿足铭腕,則返回ETIMEOUT银择,結(jié)束等待,其中abstime以與time()系統(tǒng)調(diào)用相同意義的絕對時間形式出現(xiàn)累舷,0表示格林尼治時間1970年1月1日0時0分0秒浩考。
無論哪種等待方式,都必須和一個互斥鎖配合被盈,以防止多個線程同時請求pthread_cond_wait()(或pthread_cond_timedwait()析孽,下同)的競爭條件(Race Condition)。mutex互斥鎖必須是普通鎖(PTHREAD_MUTEX_TIMED_NP)或者適應(yīng)鎖(PTHREAD_MUTEX_ADAPTIVE_NP)只怎,且在調(diào)用pthread_cond_wait()前必須由本線程加鎖(pthread_mutex_lock())袜瞬,而在更新條件等待隊列以前,mutex保持鎖定狀態(tài)身堡,并在線程掛起進(jìn)入等待前解鎖邓尤。在條件滿足從而離開pthread_cond_wait()之前,mutex將被重新加鎖贴谎,以與進(jìn)入pthread_cond_wait()前的加鎖動作對應(yīng)汞扎。
激發(fā)條件有兩種形式,pthread_cond_signal()激活一個等待該條件的線程擅这,存在多個等待線程時按入隊順序激活其中一個澈魄;而pthread_cond_broadcast()則激活所有等待線程。
3. 其他
pthread_cond_wait()和pthread_cond_timedwait()都被實(shí)現(xiàn)為取消點(diǎn)蕾哟,因此一忱,在該處等待的線程將立即重新運(yùn)行莲蜘,在重新鎖定mutex后離開pthread_cond_wait(),然后執(zhí)行取消動作帘营。也就是說如果pthread_cond_wait()被取消票渠,mutex是保持鎖定狀態(tài)的,因而需要定義退出回調(diào)函數(shù)來為其解鎖芬迄。
以下示例集中演示了互斥鎖和條件變量的結(jié)合使用问顷,以及取消對于條件等待動作的影響。在例子中禀梳,有兩個線程被啟動杜窄,并等待同一個條件變量,如果不使用退出回調(diào)函數(shù)(見范例中的注釋部分)算途,則tid2將在pthread_mutex_lock()處永久等待塞耕。如果使用回調(diào)函數(shù),則tid2的條件等待及主線程的條件激發(fā)都能正常工作嘴瓤。
pthread_mutex_t mutex;
pthread_cond_t? cond;
void * child1(void *arg)
{
????????pthread_cleanup_push(pthread_mutex_unlock,&mutex);? /* comment 1 */
????????while(1){
????????????????printf("thread 1 get running \n");
????????printf("thread 1 pthread_mutex_lock returns %d\n",
pthread_mutex_lock(&mutex));
????????pthread_cond_wait(&cond,&mutex);
????????????????????printf("thread 1 condition applied\n");
????????pthread_mutex_unlock(&mutex);
????????????????????sleep(5);
????}
????????pthread_cleanup_pop(0);???? /* comment 2 */
}
void *child2(void *arg)
{
????????while(1){
????????????????sleep(3);?????????????? /* comment 3 */
????????????????printf("thread 2 get running.\n");
????????printf("thread 2 pthread_mutex_lock returns %d\n",
pthread_mutex_lock(&mutex));
????????pthread_cond_wait(&cond,&mutex);
????????printf("thread 2 condition applied\n");
????????pthread_mutex_unlock(&mutex);
????????sleep(1);
????????}
}
int main(void)
{
????????int tid1,tid2;
????????printf("hello, condition variable test\n");
????????pthread_mutex_init(&mutex,NULL);
????????pthread_cond_init(&cond,NULL);
????????pthread_create(&tid1,NULL,child1,NULL);
????????pthread_create(&tid2,NULL,child2,NULL);
????????do{
????????sleep(2);?????????????????? /* comment 4 */
????????????????pthread_cancel(tid1);?????? /* comment 5 */
????????????????sleep(2);?????????????????? /* comment 6 */
????????pthread_cond_signal(&cond);
????}while(1);?
????????sleep(100);
????????pthread_exit(0);
}
如果不做注釋5的pthread_cancel()動作扫外,即使沒有那些sleep()延時操作,child1和child2都能正常工作廓脆。注釋3和注釋4的延遲使得child1有時間完成取消動作筛谚,從而使child2能在child1退出之后進(jìn)入請求鎖操作。如果沒有注釋1和注釋2的回調(diào)函數(shù)定義停忿,系統(tǒng)將掛起在child2請求鎖的地方驾讲;而如果同時也不做注釋3和注釋4的延時,child2能在child1完成取消動作以前得到控制席赂,從而順利執(zhí)行申請鎖的操作吮铭,但卻可能掛起在pthread_cond_wait()中,因?yàn)槠渲幸灿猩暾坢utex的操作氧枣。child1函數(shù)給出的是標(biāo)準(zhǔn)的條件變量的使用方式:回調(diào)函數(shù)保護(hù)沐兵,等待條件前鎖定别垮,pthread_cond_wait()返回后解鎖便监。
條件變量機(jī)制不是異步信號安全的,也就是說碳想,在信號處理函數(shù)中調(diào)用pthread_cond_signal()或者pthread_cond_broadcast()很可能引起死鎖烧董。
信號燈
信號燈與互斥鎖和條件變量的主要不同在于"燈"的概念,燈亮則意味著資源可用胧奔,燈滅則意味著不可用逊移。如果說后兩中同步方式側(cè)重于"等待"操作,即資源不可用的話龙填,信號燈機(jī)制則側(cè)重于點(diǎn)燈胳泉,即告知資源可用拐叉;沒有等待線程的解鎖或激發(fā)條件都是沒有意義的,而沒有等待燈亮的線程的點(diǎn)燈操作則有效扇商,且能保持燈亮狀態(tài)凤瘦。當(dāng)然,這樣的操作原語也意味著更多的開銷案铺。
信號燈的應(yīng)用除了燈亮/燈滅這種二元燈以外蔬芥,也可以采用大于1的燈數(shù),以表示資源數(shù)大于1控汉,這時可以稱之為多元燈笔诵。
1. 創(chuàng)建和注銷
POSIX信號燈標(biāo)準(zhǔn)定義了有名信號燈和無名信號燈兩種,但LinuxThreads的實(shí)現(xiàn)僅有無名燈姑子,同時有名燈除了總是可用于多進(jìn)程之間以外乎婿,在使用上與無名燈并沒有很大的區(qū)別,因此下面僅就無名燈進(jìn)行討論街佑。
int sem_init(sem_t *sem, int pshared, unsigned int value)
這是創(chuàng)建信號燈的API次酌,其中value為信號燈的初值,pshared表示是否為多進(jìn)程共享而不僅僅是用于一個進(jìn)程舆乔。LinuxThreads沒有實(shí)現(xiàn)多進(jìn)程共享信號燈岳服,因此所有非0值的pshared輸入都將使sem_init()返回-1,且置errno為ENOSYS希俩。初始化好的信號燈由sem變量表征吊宋,用于以下點(diǎn)燈、滅燈操作颜武。
int sem_destroy(sem_t * sem)
被注銷的信號燈sem要求已沒有線程在等待該信號燈璃搜,否則返回-1,且置errno為EBUSY鳞上。除此之外这吻,LinuxThreads的信號燈注銷函數(shù)不做其他動作。
2. 點(diǎn)燈和滅燈
int sem_post(sem_t * sem)
點(diǎn)燈操作將信號燈值原子地加1篙议,表示增加一個可訪問的資源唾糯。
int sem_wait(sem_t * sem)
int sem_trywait(sem_t * sem)
sem_wait()為等待燈亮操作,等待燈亮(信號燈值大于0)鬼贱,然后將信號燈原子地減1移怯,并返回。sem_trywait()為sem_wait()的非阻塞版这难,如果信號燈計數(shù)大于0舟误,則原子地減1并返回0,否則立即返回-1姻乓,errno置為EAGAIN嵌溢。
3. 獲取燈值
int sem_getvalue(sem_t * sem, int * sval)
讀取sem中的燈計數(shù)眯牧,存于*sval中,并返回0赖草。
4. 其他
sem_wait()被實(shí)現(xiàn)為取消點(diǎn)炸站,而且在支持原子"比較且交換"指令的體系結(jié)構(gòu)上,sem_post()是唯一能用于異步信號處理函數(shù)的POSIX異步信號安全的API疚顷。
異步信號
由于LinuxThreads是在核外使用核內(nèi)輕量級進(jìn)程實(shí)現(xiàn)的線程旱易,所以基于內(nèi)核的異步信號操作對于線程也是有效的。但同時腿堤,由于異步信號總是實(shí)際發(fā)往某個進(jìn)程阀坏,所以無法實(shí)現(xiàn)POSIX標(biāo)準(zhǔn)所要求的"信號到達(dá)某個進(jìn)程,然后再由該進(jìn)程將信號分發(fā)到所有沒有阻塞該信號的線程中"原語笆檀,而是只能影響到其中一個線程忌堂。
POSIX異步信號同時也是一個標(biāo)準(zhǔn)C庫提供的功能,主要包括信號集管理(sigemptyset()酗洒、sigfillset()士修、sigaddset()、sigdelset()樱衷、sigismember()等)棋嘲、信號處理函數(shù)安裝(sigaction())、信號阻塞控制(sigprocmask())矩桂、被阻塞信號查詢(sigpending())沸移、信號等待(sigsuspend())等,它們與發(fā)送信號的kill()等函數(shù)配合就能實(shí)現(xiàn)進(jìn)程間異步信號功能侄榴。LinuxThreads圍繞線程封裝了sigaction()何raise()雹锣,本節(jié)集中討論LinuxThreads中擴(kuò)展的異步信號函數(shù),包括pthread_sigmask()癞蚕、pthread_kill()和sigwait()三個函數(shù)蕊爵。毫無疑問,所有POSIX異步信號函數(shù)對于線程都是可用的桦山。
int pthread_sigmask(int how, const sigset_t *newmask, sigset_t *oldmask)
設(shè)置線程的信號屏蔽碼攒射,語義與sigprocmask()相同,但對不允許屏蔽的Cancel信號和不允許響應(yīng)的Restart信號進(jìn)行了保護(hù)度苔。被屏蔽的信號保存在信號隊列中匆篓,可由sigpending()函數(shù)取出浑度。
int pthread_kill(pthread_t thread, int signo)
向thread號線程發(fā)送signo信號寇窑。實(shí)現(xiàn)中在通過thread線程號定位到對應(yīng)進(jìn)程號以后使用kill()系統(tǒng)調(diào)用完成發(fā)送。
int sigwait(const sigset_t *set, int *sig)
掛起線程箩张,等待set中指定的信號之一到達(dá)甩骏,并將到達(dá)的信號存入*sig中窗市。POSIX標(biāo)準(zhǔn)建議在調(diào)用sigwait()等待信號以前,進(jìn)程中所有線程都應(yīng)屏蔽該信號饮笛,以保證僅有sigwait()的調(diào)用者獲得該信號咨察,因此,對于需要等待同步的異步信號福青,總是應(yīng)該在創(chuàng)建任何線程以前調(diào)用pthread_sigmask()屏蔽該信號的處理摄狱。而且,調(diào)用sigwait()期間无午,原來附接在該信號上的信號處理函數(shù)不會被調(diào)用媒役。
如果在等待期間接收到Cancel信號,則立即退出等待宪迟,也就是說sigwait()被實(shí)現(xiàn)為取消點(diǎn)酣衷。
其他同步方式
除了上述討論的同步方式以外,其他很多進(jìn)程間通信手段對于LinuxThreads也是可用的次泽,比如基于文件系統(tǒng)的IPC(管道穿仪、Unix域Socket等)、消息隊列(Sys.V或者Posix的)意荤、System V的信號燈等啊片。只有一點(diǎn)需要注意,LinuxThreads在核內(nèi)是作為共享存儲區(qū)玖像、共享文件系統(tǒng)屬性钠龙、共享信號處理、共享文件描述符的獨(dú)立進(jìn)程看待的御铃。