【轉(zhuǎn)】Linux C++的多線程編程

轉(zhuǎn)自:Youther
https://www.cnblogs.com/youtherhome/archive/2013/03/17/2964195.html

1. 引言

線程(thread)技術(shù)早在60年代就被提出枫夺,但真正應(yīng)用多線程到操作系統(tǒng)中去,是在80年代中期,solaris是這方面的佼佼者牍疏。傳統(tǒng)的Unix也支持線程的概念项乒,但是在一個進(jìn)程(process)中只允許有一個線程扮念,這樣多線程就意味著多進(jìn)程⊥谎現(xiàn)在什黑,多線程技術(shù)已經(jīng)被許多操作系統(tǒng)所支持崎淳,包括Windows/NT,當(dāng)然愕把,也包括Linux拣凹。
  為什么有了進(jìn)程的概念后,還要再引入線程呢恨豁?使用多線程到底有哪些好處嚣镜?什么的系統(tǒng)應(yīng)該選用多線程?我們首先必須回答這些問題橘蜜。
  使用多線程的理由之一是和進(jìn)程相比菊匿,它是一種非常"節(jié)儉"的多任務(wù)操作方式。我們知道计福,在Linux系統(tǒng)下跌捆,啟動一個新的進(jìn)程必須分配給它獨(dú)立的地址空間,建立眾多的數(shù)據(jù)表來維護(hù)它的代碼段象颖、堆棧段和數(shù)據(jù)段佩厚,這是一種"昂貴"的多任務(wù)工作方式。而運(yùn)行于一個進(jìn)程中的多個線程说订,它們彼此之間使用相同的地址空間抄瓦,共享大部分?jǐn)?shù)據(jù),啟動一個線程所花費(fèi)的空間遠(yuǎn)遠(yuǎn)小于啟動一個進(jìn)程所花費(fèi)的空間克蚂,而且闺鲸,線程間彼此切換所需的時(shí)間也遠(yuǎn)遠(yuǎn)小于進(jìn)程間切換所需要的時(shí)間筋讨。據(jù)統(tǒng)計(jì)埃叭,總的說來,一個進(jìn)程的開銷大約是一個線程開銷的30倍左右悉罕,當(dāng)然赤屋,在具體的系統(tǒng)上立镶,這個數(shù)據(jù)可能會有較大的區(qū)別。
  使用多線程的理由之二是線程間方便的通信機(jī)制类早。對不同進(jìn)程來說媚媒,它們具有獨(dú)立的數(shù)據(jù)空間,要進(jìn)行數(shù)據(jù)的傳遞只能通過通信的方式進(jìn)行涩僻,這種方式不僅費(fèi)時(shí)缭召,而且很不方便。線程則不然逆日,由于同一進(jìn)程下的線程之間共享數(shù)據(jù)空間嵌巷,所以一個線程的數(shù)據(jù)可以直接為其它線程所用,這不僅快捷室抽,而且方便搪哪。當(dāng)然,數(shù)據(jù)的共享也帶來其他一些問題坪圾,有的變量不能同時(shí)被兩個線程所修改晓折,有的子程序中聲明為static的數(shù)據(jù)更有可能給多線程程序帶來災(zāi)難性的打擊,這些正是編寫多線程程序時(shí)最需要注意的地方兽泄。
  除了以上所說的優(yōu)點(diǎn)外漓概,不和進(jìn)程比較,多線程程序作為一種多任務(wù)病梢、并發(fā)的工作方式垛耳,當(dāng)然有以下的優(yōu)點(diǎn):
  1) 提高應(yīng)用程序響應(yīng)。這對圖形界面的程序尤其有意義飘千,當(dāng)一個操作耗時(shí)很長時(shí)堂鲜,整個系統(tǒng)都會等待這個操作,此時(shí)程序不會響應(yīng)鍵盤护奈、鼠標(biāo)缔莲、菜單的操作,而使用多線程技術(shù)霉旗,將耗時(shí)長的操作(time consuming)置于一個新的線程痴奏,可以避免這種尷尬的情況。
  2) 使多CPU系統(tǒng)更加有效厌秒。操作系統(tǒng)會保證當(dāng)線程數(shù)不大于CPU數(shù)目時(shí)读拆,不同的線程運(yùn)行于不同的CPU上。
  3) 改善程序結(jié)構(gòu)鸵闪。一個既長又復(fù)雜的進(jìn)程可以考慮分為多個線程檐晕,成為幾個獨(dú)立或半獨(dú)立的運(yùn)行部分,這樣的程序會利于理解和修改。
  下面我們先來嘗試編寫一個簡單的多線程程序辟灰。

2. 簡單的多線程編程

Linux系統(tǒng)下的多線程遵循POSIX線程接口个榕,稱為pthread。編寫Linux下的多線程程序芥喇,需要使用頭文件pthread.h西采,連接時(shí)需要使用庫libpthread.a。順便說一下继控,Linux下pthread的實(shí)現(xiàn)是通過系統(tǒng)調(diào)用clone()來實(shí)現(xiàn)的械馆。clone()是Linux所特有的系統(tǒng)調(diào)用,它的使用方式類似fork武通,關(guān)于clone()的詳細(xì)情況狱杰,有興趣的讀者可以去查看有關(guān)文檔說明。下面我們展示一個最簡單的多線程程序threads.cpp厅须。

//Threads.cpp
#include <iostream>
#include <unistd.h>
#include <pthread.h>
using namespace std;

void *thread(void *ptr)
{
    for(int i = 0;i < 3;i++) {
        sleep(1);
        cout << "This is a pthread." << endl;
    }
    return 0;
}

int main() {
    pthread_t id;
    int ret = pthread_create(&id, NULL, thread, NULL);
    if(ret) {
        cout << "Create pthread error!" << endl;
        return 1;
    }
    for(int i = 0;i < 3;i++) {
        cout <<  "This is the main process." << endl;
        sleep(1);
    }
    pthread_join(id, NULL);
    return 0;
}

我們編譯并運(yùn)行此程序仿畸,可以得到如下結(jié)果:

  This is the main process.
  This is a pthread.
  This is the main process.
  This is the main process.
  This is a pthread.
  This is a pthread.

再次運(yùn)行,我們可能得到如下結(jié)果:

  This is a pthread.
  This is the main process.
  This is a pthread.
  This is the main process.
  This is a pthread.
  This is the main process.

前后兩次結(jié)果不一樣朗和,這是兩個線程爭奪CPU資源的結(jié)果错沽。上面的示例中,我們使用到了兩個函數(shù)眶拉,pthread_create和pthread_join千埃,并聲明了一個pthread_t型的變量。
  pthread_t在頭文件/usr/include/bits/pthreadtypes.h中定義:

typedef unsigned long int pthread_t;

它是一個線程的標(biāo)識符忆植。函數(shù)pthread_create用來創(chuàng)建一個線程放可,它的原型為:

extern int pthread_create __P ((pthread_t *__thread, __const pthread_attr_t *__attr,
void *(*__start_routine) (void *), void *__arg));

第一個參數(shù)為指向線程標(biāo)識符的指針,第二個參數(shù)用來設(shè)置線程屬性朝刊,第三個參數(shù)是線程運(yùn)行函數(shù)的起始地址耀里,最后一個參數(shù)是運(yùn)行函數(shù)的參數(shù)。這里拾氓,我們的函數(shù)thread不需要參數(shù)冯挎,所以最后一個參數(shù)設(shè)為空指針。第二個參數(shù)我們也設(shè)為空指針咙鞍,這樣將生成默認(rèn)屬性的線程房官。對線程屬性的設(shè)定和修改我們將在下一節(jié)闡述。當(dāng)創(chuàng)建線程成功時(shí)续滋,函數(shù)返回0翰守,若不為0則說明創(chuàng)建線程失敗,常見的錯誤返回代碼為EAGAIN和EINVAL疲酌。前者表示系統(tǒng)限制創(chuàng)建新的線程蜡峰,例如線程數(shù)目過多了;后者表示第二個參數(shù)代表的線程屬性值非法。創(chuàng)建線程成功后事示,新創(chuàng)建的線程則運(yùn)行參數(shù)三和參數(shù)四確定的函數(shù)早像,原來的線程則繼續(xù)運(yùn)行下一行代碼僻肖。
  函數(shù)pthread_join用來等待一個線程的結(jié)束肖爵。函數(shù)原型為:

extern int pthread_join __P ((pthread_t __th, void **__thread_return));

第一個參數(shù)為被等待的線程標(biāo)識符,第二個參數(shù)為一個用戶定義的指針臀脏,它可以用來存儲被等待線程的返回值劝堪。這個函數(shù)是一個線程阻塞的函數(shù),調(diào)用它的函數(shù)將一直等待到被等待的線程結(jié)束為止揉稚,當(dāng)函數(shù)返回時(shí)秒啦,被等待線程的資源被收回。一個線程的結(jié)束有兩種途徑搀玖,一種是象我們上面的例子一樣余境,函數(shù)結(jié)束了,調(diào)用它的線程也就結(jié)束了灌诅;另一種方式是通過函數(shù)pthread_exit來實(shí)現(xiàn)芳来。它的函數(shù)原型為:

extern void pthread_exit __P ((void *__retval)) __attribute__ ((__noreturn__));

唯一的參數(shù)是函數(shù)的返回代碼,只要pthread_join中的第二個參數(shù)thread_return不是NULL猜拾,這個值將被傳遞給thread_return即舌。最后要說明的是,一個線程不能被多個線程等待挎袜,否則第一個接收到信號的線程成功返回顽聂,其余調(diào)用pthread_join的線程則返回錯誤代碼ESRCH。
  在這一節(jié)里盯仪,我們編寫了一個最簡單的線程紊搪,并掌握了最常用的三個函數(shù)pthread_create,pthread_join和pthread_exit全景。下面嗦明,我們來了解線程的一些常用屬性以及如何設(shè)置這些屬性。

3. 修改線程的屬性

在上一節(jié)的例子里蚪燕,我們用pthread_create函數(shù)創(chuàng)建了一個線程娶牌,在這個線程中,我們使用了默認(rèn)參數(shù)馆纳,即將該函數(shù)的第二個參數(shù)設(shè)為NULL诗良。的確,對大多數(shù)程序來說鲁驶,使用默認(rèn)屬性就夠了鉴裹,但我們還是有必要來了解一下線程的有關(guān)屬性。
  屬性結(jié)構(gòu)為pthread_attr_t,它同樣在頭文件/usr/include/pthread.h中定義径荔,喜歡追根問底的人可以自己去查看督禽。屬性值不能直接設(shè)置,須使用相關(guān)函數(shù)進(jìn)行操作总处,初始化的函數(shù)為pthread_attr_init狈惫,這個函數(shù)必須在pthread_create函數(shù)之前調(diào)用。屬性對象主要包括是否綁定鹦马、是否分離胧谈、堆棧地址、堆棧大小荸频、優(yōu)先級菱肖。默認(rèn)的屬性為非綁定、非分離旭从、缺省1M的堆棧稳强、與父進(jìn)程同樣級別的優(yōu)先級。
  關(guān)于線程的綁定和悦,牽涉到另外一個概念:輕進(jìn)程(LWP:Light Weight Process)退疫。輕進(jìn)程可以理解為內(nèi)核線程,它位于用戶層和系統(tǒng)層之間摹闽。系統(tǒng)對線程資源的分配蹄咖、對線程的控制是通過輕進(jìn)程來實(shí)現(xiàn)的,一個輕進(jìn)程可以控制一個或多個線程付鹿。默認(rèn)狀況下澜汤,啟動多少輕進(jìn)程、哪些輕進(jìn)程來控制哪些線程是由系統(tǒng)來控制的舵匾,這種狀況即稱為非綁定的俊抵。綁定狀況下,則顧名思義坐梯,即某個線程固定的"綁"在一個輕進(jìn)程之上徽诲。被綁定的線程具有較高的響應(yīng)速度,這是因?yàn)镃PU時(shí)間片的調(diào)度是面向輕進(jìn)程的吵血,綁定的線程可以保證在需要的時(shí)候它總有一個輕進(jìn)程可用谎替。通過設(shè)置被綁定的輕進(jìn)程的優(yōu)先級和調(diào)度級可以使得綁定的線程滿足諸如實(shí)時(shí)反應(yīng)之類的要求。
  設(shè)置線程綁定狀態(tài)的函數(shù)為pthread_attr_setscope蹋辅,它有兩個參數(shù)钱贯,第一個是指向?qū)傩越Y(jié)構(gòu)的指針,第二個是綁定類型侦另,它有兩個取值:PTHREAD_SCOPE_SYSTEM(綁定的)和PTHREAD_SCOPE_PROCESS(非綁定的)秩命。下面的代碼即創(chuàng)建了一個綁定的線程尉共。

#include <pthread.h>
pthread_attr_t attr;
pthread_t tid;
/*初始化屬性值,均設(shè)為默認(rèn)值*/
pthread_attr_init(&attr);
pthread_attr_setscope(&attr, PTHREAD_SCOPE_SYSTEM);
pthread_create(&tid, &attr, (void *) my_function, NULL);

線程的分離狀態(tài)決定一個線程以什么樣的方式來終止自己弃锐。在上面的例子中袄友,我們采用了線程的默認(rèn)屬性,即為非分離狀態(tài)霹菊,這種情況下剧蚣,原有的線程等待創(chuàng)建的線程結(jié)束。只有當(dāng)pthread_join()函數(shù)返回時(shí)浇辜,創(chuàng)建的線程才算終止券敌,才能釋放自己占用的系統(tǒng)資源唾戚。而分離線程不是這樣子的柳洋,它沒有被其他的線程所等待,自己運(yùn)行結(jié)束了叹坦,線程也就終止了熊镣,馬上釋放系統(tǒng)資源。程序員應(yīng)該根據(jù)自己的需要募书,選擇適當(dāng)?shù)姆蛛x狀態(tài)绪囱。設(shè)置線程分離狀態(tài)的函數(shù)為pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate)。第二個參數(shù)可選為PTHREAD_CREATE_DETACHED(分離線程)和 PTHREAD _CREATE_JOINABLE(非分離線程)莹捡。這里要注意的一點(diǎn)是鬼吵,如果設(shè)置一個線程為分離線程,而這個線程運(yùn)行又非忱河快齿椅,它很可能在pthread_create函數(shù)返回之前就終止了,它終止以后就可能將線程號和系統(tǒng)資源移交給其他的線程使用启泣,這樣調(diào)用pthread_create的線程就得到了錯誤的線程號涣脚。要避免這種情況可以采取一定的同步措施,最簡單的方法之一是可以在被創(chuàng)建的線程里調(diào)用pthread_cond_timewait函數(shù)寥茫,讓這個線程等待一會兒遣蚀,留出足夠的時(shí)間讓函數(shù)pthread_create返回。設(shè)置一段等待時(shí)間纱耻,是在多線程編程里常用的方法找默。但是注意不要使用諸如wait()之類的函數(shù),它們是使整個進(jìn)程睡眠琳袄,并不能解決線程同步的問題灰蛙。

另外一個可能常用的屬性是線程的優(yōu)先級,它存放在結(jié)構(gòu)sched_param中限次。用函數(shù)pthread_attr_getschedparam和函數(shù)pthread_attr_setschedparam進(jìn)行存放芒涡,一般說來柴灯,我們總是先取優(yōu)先級,對取得的值修改后再存放回去费尽。下面即是一段簡單的例子赠群。

#include <pthread.h>
#include <sched.h>
pthread_attr_t attr;
pthread_t tid;
sched_param param;
int newprio=20;
pthread_attr_init(&amp;attr);
pthread_attr_getschedparam(&attr, &param);
param.sched_priority=newprio;
pthread_attr_setschedparam(&attr, &param);
pthread_create(&tid, &attr, (void *)myfunction, myarg);

4. 線程的數(shù)據(jù)處理

和進(jìn)程相比,線程的最大優(yōu)點(diǎn)之一是數(shù)據(jù)的共享性旱幼,各個進(jìn)程共享父進(jìn)程處沿襲的數(shù)據(jù)段查描,可以方便的獲得、修改數(shù)據(jù)柏卤。但這也給多線程編程帶來了許多問題冬三。我們必須當(dāng)心有多個不同的進(jìn)程訪問相同的變量。許多函數(shù)是不可重入的缘缚,即同時(shí)不能運(yùn)行一個函數(shù)的多個拷貝(除非使用不同的數(shù)據(jù)段)勾笆。在函數(shù)中聲明的靜態(tài)變量常常帶來問題,函數(shù)的返回值也會有問題桥滨。因?yàn)槿绻祷氐氖呛瘮?shù)內(nèi)部靜態(tài)聲明的空間的地址窝爪,則在一個線程調(diào)用該函數(shù)得到地址后使用該地址指向的數(shù)據(jù)時(shí),別的線程可能調(diào)用此函數(shù)并修改了這一段數(shù)據(jù)齐媒。在進(jìn)程中共享的變量必須用關(guān)鍵字volatile來定義蒲每,這是為了防止編譯器在優(yōu)化時(shí)(如gcc中使用-OX參數(shù))改變它們的使用方式。為了保護(hù)變量喻括,我們必須使用信號量邀杏、互斥等方法來保證我們對變量的正確使用。下面唬血,我們就逐步介紹處理線程數(shù)據(jù)時(shí)的有關(guān)知識望蜡。

4.1 線程數(shù)據(jù)

在單線程的程序里,有兩種基本的數(shù)據(jù):全局變量和局部變量刁品。但在多線程程序里泣特,還有第三種數(shù)據(jù)類型:線程數(shù)據(jù)(TSD: Thread-Specific Data)。它和全局變量很象挑随,在線程內(nèi)部状您,各個函數(shù)可以象使用全局變量一樣調(diào)用它,但它對線程外部的其它線程是不可見的兜挨。這種數(shù)據(jù)的必要性是顯而易見的膏孟。例如我們常見的變量errno,它返回標(biāo)準(zhǔn)的出錯信息拌汇。它顯然不能是一個局部變量柒桑,幾乎每個函數(shù)都應(yīng)該可以調(diào)用它;但它又不能是一個全局變量噪舀,否則在A線程里輸出的很可能是B線程的出錯信息魁淳。要實(shí)現(xiàn)諸如此類的變量飘诗,我們就必須使用線程數(shù)據(jù)。我們?yōu)槊總€線程數(shù)據(jù)創(chuàng)建一個鍵界逛,它和這個鍵相關(guān)聯(lián)昆稿,在各個線程里,都使用這個鍵來指代線程數(shù)據(jù)息拜,但在不同的線程里溉潭,這個鍵代表的數(shù)據(jù)是不同的,在同一個線程里少欺,它代表同樣的數(shù)據(jù)內(nèi)容喳瓣。
  和線程數(shù)據(jù)相關(guān)的函數(shù)主要有4個:創(chuàng)建一個鍵;為一個鍵指定線程數(shù)據(jù)赞别;從一個鍵讀取線程數(shù)據(jù)畏陕;刪除鍵。
  創(chuàng)建鍵的函數(shù)原型為:

extern int pthread_key_create __P ((pthread_key_t *__key,void (*__destr_function) (void *)));

第一個參數(shù)為指向一個鍵值的指針氯庆,第二個參數(shù)指明了一個destructor函數(shù)蹭秋,如果這個參數(shù)不為空扰付,那么當(dāng)每個線程結(jié)束時(shí)堤撵,系統(tǒng)將調(diào)用這個函數(shù)來釋放綁定在這個鍵上的內(nèi)存塊。這個函數(shù)常和函數(shù)pthread_once ((pthread_once_tonce_control, void (initroutine) (void)))一起使用羽莺,為了讓這個鍵只被創(chuàng)建一次实昨。函數(shù)pthread_once聲明一個初始化函數(shù),第一次調(diào)用pthread_once時(shí)它執(zhí)行這個函數(shù)盐固,以后的調(diào)用將被它忽略荒给。

在下面的例子中,我們創(chuàng)建一個鍵刁卜,并將它和某個數(shù)據(jù)相關(guān)聯(lián)志电。我們要定義一個函數(shù)createWindow,這個函數(shù)定義一個圖形窗口(數(shù)據(jù)類型為Fl_Window *蛔趴,這是圖形界面開發(fā)工具FLTK中的數(shù)據(jù)類型)挑辆。 由于各個線程都會調(diào)用這個函數(shù),所以我們使用線程數(shù)據(jù)孝情。

/* 聲明一個鍵*/
pthread_key_t myWinKey;
/* 函數(shù) createWindow */
void createWindow ( void ) {
    Fl_Window * win;
    static pthread_once_t once= PTHREAD_ONCE_INIT;
    /* 調(diào)用函數(shù)createMyKey鱼蝉,創(chuàng)建鍵*/
    pthread_once ( & once, createMyKey) ;
    /*win指向一個新建立的窗口*/
    win=new Fl_Window( 0, 0, 100, 100, "MyWindow");
    /* 對此窗口作一些可能的設(shè)置工作,如大小箫荡、位置魁亦、名稱等*/
    setWindow(win);
    /* 將窗口指針值綁定在鍵myWinKey上*/
    pthread_setpecific ( myWinKey, win);
}

/* 函數(shù) createMyKey,創(chuàng)建一個鍵羔挡,并指定了destructor */
void createMyKey ( void ) {
    pthread_keycreate(&myWinKey, freeWinKey);
}

/* 函數(shù) freeWinKey洁奈,釋放空間*/
void freeWinKey ( Fl_Window * win){
    delete win;
}

這樣间唉,在不同的線程中調(diào)用函數(shù)createMyWin,都可以得到在線程內(nèi)部均可見的窗口變量利术,這個變量通過函數(shù)pthread_getspecific得到终吼。在上面的例子中,我們已經(jīng)使用了函數(shù)pthread_setspecific來將線程數(shù)據(jù)和一個鍵綁定在一起氯哮。這兩個函數(shù)的原型如下:

extern int pthread_setspecific __P ((pthread_key_t __key,__const void *__pointer));
extern void *pthread_getspecific __P ((pthread_key_t __key));

這兩個函數(shù)的參數(shù)意義和使用方法是顯而易見的际跪。要注意的是,用pthread_setspecific為一個鍵指定新的線程數(shù)據(jù)時(shí)喉钢,必須自己釋放原有的線程數(shù)據(jù)以回收空間姆打。這個過程函數(shù)pthread_key_delete用來刪除一個鍵,這個鍵占用的內(nèi)存將被釋放肠虽,但同樣要注意的是幔戏,它只釋放鍵占用的內(nèi)存,并不釋放該鍵關(guān)聯(lián)的線程數(shù)據(jù)所占用的內(nèi)存資源税课,而且它也不會觸發(fā)函數(shù)pthread_key_create中定義的destructor函數(shù)闲延。線程數(shù)據(jù)的釋放必須在釋放鍵之前完成。

4.2 互斥鎖

互斥鎖用來保證一段時(shí)間內(nèi)只有一個線程在執(zhí)行一段代碼韩玩。必要性顯而易見:假設(shè)各個線程向同一個文件順序?qū)懭霐?shù)據(jù)垒玲,最后得到的結(jié)果一定是災(zāi)難性的。
  我們先看下面一段代碼找颓。這是一個讀/寫程序合愈,它們公用一個緩沖區(qū),并且我們假定一個緩沖區(qū)只能保存一條信息击狮。即緩沖區(qū)只有兩個狀態(tài):有信息或沒有信息佛析。

void reader_function ( void );
void writer_function ( void ); 
char buffer;
int buffer_has_item=0;
pthread_mutex_t mutex;
struct timespec delay;

void main ( void ){
    pthread_t reader;
    /* 定義延遲時(shí)間*/
    delay.tv_sec = 2;
    delay.tv_nec = 0;
    /* 用默認(rèn)屬性初始化一個互斥鎖對象*/
    pthread_mutex_init (&mutex,NULL);
    pthread_create(&reader, pthread_attr_default, (void *)&reader_function), NULL);
    writer_function( );
}

void writer_function (void){
    while(1){
        /* 鎖定互斥鎖*/
        pthread_mutex_lock (&mutex);
        if (buffer_has_item==0){
            buffer=make_new_item( );
            buffer_has_item=1;
        }
        /* 打開互斥鎖*/
        pthread_mutex_unlock(&mutex);
        pthread_delay_np(&delay);
    }
}

void reader_function(void){
    while(1){
        pthread_mutex_lock(&mutex);
        if(buffer_has_item==1){
            consume_item(buffer);
            buffer_has_item=0;
        }
        pthread_mutex_unlock(&mutex);
        pthread_delay_np(&delay);
    }
}

這里聲明了互斥鎖變量mutex,結(jié)構(gòu)pthread_mutex_t為不公開的數(shù)據(jù)類型彪蓬,其中包含一個系統(tǒng)分配的屬性對象寸莫。函數(shù)pthread_mutex_init用來生成一個互斥鎖。NULL參數(shù)表明使用默認(rèn)屬性档冬。如果需要聲明特定屬性的互斥鎖膘茎,須調(diào)用函數(shù)pthread_mutexattr_init。函數(shù)pthread_mutexattr_setpshared和函數(shù)pthread_mutexattr_settype用來設(shè)置互斥鎖屬性捣郊。前一個函數(shù)設(shè)置屬性pshared辽狈,它有兩個取值,PTHREAD_PROCESS_PRIVATE和PTHREAD_PROCESS_SHARED呛牲。前者用來不同進(jìn)程中的線程同步刮萌,后者用于同步本進(jìn)程的不同線程。在上面的例子中娘扩,我們使用的是默認(rèn)屬性PTHREAD_PROCESS_ PRIVATE着茸。后者用來設(shè)置互斥鎖類型壮锻,可選的類型有PTHREAD_MUTEX_NORMAL、PTHREAD_MUTEX_ERRORCHECK涮阔、PTHREAD_MUTEX_RECURSIVE和PTHREAD _MUTEX_DEFAULT猜绣。它們分別定義了不同的上鎖、解鎖機(jī)制敬特,一般情況下掰邢,選用最后一個默認(rèn)屬性。
  pthread_mutex_lock聲明開始用互斥鎖上鎖伟阔,此后的代碼直至調(diào)用pthread_mutex_unlock為止辣之,均被上鎖,即同一時(shí)間只能被一個線程調(diào)用執(zhí)行皱炉。當(dāng)一個線程執(zhí)行到pthread_mutex_lock處時(shí)怀估,如果該鎖此時(shí)被另一個線程使用,那此線程被阻塞合搅,即程序?qū)⒌却搅硪粋€線程釋放此互斥鎖多搀。在上面的例子中,我們使用了pthread_delay_np函數(shù)灾部,讓線程睡眠一段時(shí)間康铭,就是為了防止一個線程始終占據(jù)此函數(shù)。
  上面的例子非常簡單梳猪,就不再介紹了麻削,需要提出的是在使用互斥鎖的過程中很有可能會出現(xiàn)死鎖:兩個線程試圖同時(shí)占用兩個資源,并按不同的次序鎖定相應(yīng)的互斥鎖春弥,例如兩個線程都需要鎖定互斥鎖1和互斥鎖2,a線程先鎖定互斥鎖1叠荠,b線程先鎖定互斥鎖2匿沛,這時(shí)就出現(xiàn)了死鎖。此時(shí)我們可以使用函數(shù)pthread_mutex_trylock榛鼎,它是函數(shù)pthread_mutex_lock的非阻塞版本逃呼,當(dāng)它發(fā)現(xiàn)死鎖不可避免時(shí),它會返回相應(yīng)的信息者娱,程序員可以針對死鎖做出相應(yīng)的處理抡笼。另外不同的互斥鎖類型對死鎖的處理不一樣,但最主要的還是要程序員自己在程序設(shè)計(jì)注意這一點(diǎn)黄鳍。

4.3 條件變量

前一節(jié)中我們講述了如何使用互斥鎖來實(shí)現(xiàn)線程間數(shù)據(jù)的共享和通信推姻,互斥鎖一個明顯的缺點(diǎn)是它只有兩種狀態(tài):鎖定和非鎖定。而條件變量通過允許線程阻塞和等待另一個線程發(fā)送信號的方法彌補(bǔ)了互斥鎖的不足框沟,它常和互斥鎖一起使用藏古。使用時(shí)增炭,條件變量被用來阻塞一個線程,當(dāng)條件不滿足時(shí)拧晕,線程往往解開相應(yīng)的互斥鎖并等待條件發(fā)生變化隙姿。一旦其它的某個線程改變了條件變量,它將通知相應(yīng)的條件變量喚醒一個或多個正被此條件變量阻塞的線程厂捞。這些線程將重新鎖定互斥鎖并重新測試條件是否滿足输玷。一般說來,條件變量被用來進(jìn)行線承間的同步靡馁。
  條件變量的結(jié)構(gòu)為pthread_cond_t饲嗽,函數(shù)pthread_cond_init()被用來初始化一個條件變量。它的原型為:

extern int pthread_cond_init __P ((pthread_cond_t *__cond,__const pthread_condattr_t *__cond_attr));

其中cond是一個指向結(jié)構(gòu)pthread_cond_t的指針奈嘿,cond_attr是一個指向結(jié)構(gòu)pthread_condattr_t的指針貌虾。結(jié)構(gòu)pthread_condattr_t是條件變量的屬性結(jié)構(gòu),和互斥鎖一樣我們可以用它來設(shè)置條件變量是進(jìn)程內(nèi)可用還是進(jìn)程間可用裙犹,默認(rèn)值是PTHREAD_ PROCESS_PRIVATE尽狠,即此條件變量被同一進(jìn)程內(nèi)的各個線程使用。注意初始化條件變量只有未被使用時(shí)才能重新初始化或被釋放叶圃。釋放一個條件變量的函數(shù)為pthread_cond_ destroy(pthread_cond_t cond)袄膏。 
  函數(shù)pthread_cond_wait()使線程阻塞在一個條件變量上掺冠。

它的函數(shù)原型為:

extern int pthread_cond_wait __P ((pthread_cond_t *__cond,
pthread_mutex_t *__mutex));

線程解開mutex指向的鎖并被條件變量cond阻塞沉馆。線程可以被函數(shù)pthread_cond_signal和函數(shù)pthread_cond_broadcast喚醒,但是要注意的是德崭,條件變量只是起阻塞和喚醒線程的作用斥黑,具體的判斷條件還需用戶給出,例如一個變量是否為0等等锌奴,這一點(diǎn)我們從后面的例子中可以看到。線程被喚醒后鹿蜀,它將重新檢查判斷條件是否滿足,如果還不滿足服球,一般說來線程應(yīng)該仍阻塞在這里茴恰,被等待被下一次喚醒。這個過程一般用while語句實(shí)現(xiàn)斩熊。
  另一個用來阻塞線程的函數(shù)是pthread_cond_timedwait()往枣,它的原型為:

extern int pthread_cond_timedwait __P ((pthread_cond_t *__cond,
pthread_mutex_t *__mutex, __const struct timespec *__abstime));

它比函數(shù)pthread_cond_wait()多了一個時(shí)間參數(shù),經(jīng)歷abstime段時(shí)間后,即使條件變量不滿足婉商,阻塞也被解除似忧。
  函數(shù)pthread_cond_signal()的原型為:

extern int pthread_cond_signal __P ((pthread_cond_t *__cond));

它用來釋放被阻塞在條件變量cond上的一個線程。多個線程阻塞在此條件變量上時(shí)丈秩,哪一個線程被喚醒是由線程的調(diào)度策略所決定的盯捌。要注意的是,必須用保護(hù)條件變量的互斥鎖來保護(hù)這個函數(shù)蘑秽,否則條件滿足信號又可能在測試條件和調(diào)用pthread_cond_wait函數(shù)之間被發(fā)出饺著,從而造成無限制的等待。下面是使用函數(shù)pthread_cond_wait()和函數(shù)

pthread_cond_signal()的一個簡單的例子肠牲。
pthread_mutex_t count_lock;
pthread_cond_t count_nonzero;
unsigned count;
decrement_count () {
    pthread_mutex_lock (&count_lock);
    while(count==0) 
        pthread_cond_wait( &count_nonzero, &count_lock);
    count=count -1;
    pthread_mutex_unlock (&count_lock);
}

increment_count(){
    pthread_mutex_lock(&count_lock);
    if(count==0)
        pthread_cond_signal(&count_nonzero);
    count=count+1;
    pthread_mutex_unlock(&count_lock);
}

count值為0時(shí)幼衰,decrement函數(shù)在pthread_cond_wait處被阻塞,并打開互斥鎖count_lock缀雳。此時(shí)渡嚣,當(dāng)調(diào)用到函數(shù)increment_count時(shí),pthread_cond_signal()函數(shù)改變條件變量肥印,告知decrement_count()停止阻塞识椰。讀者可以試著讓兩個線程分別運(yùn)行這兩個函數(shù),看看會出現(xiàn)什么樣的結(jié)果深碱。
  函數(shù)pthread_cond_broadcast(pthread_cond_t *cond)用來喚醒所有被阻塞在條件變量cond上的線程腹鹉。這些線程被喚醒后將再次競爭相應(yīng)的互斥鎖,所以必須小心使用這個函數(shù)敷硅。

4.4 信號量

信號量本質(zhì)上是一個非負(fù)的整數(shù)計(jì)數(shù)器功咒,它被用來控制對公共資源的訪問。當(dāng)公共資源增加時(shí)绞蹦,調(diào)用函數(shù)sem_post()增加信號量力奋。只有當(dāng)信號量值大于0時(shí),才能使用公共資源刊侯,使用后,函數(shù)sem_wait()減少信號量藕届。函數(shù)sem_trywait()和函數(shù)pthread_ mutex_trylock()起同樣的作用休偶,它是函數(shù)sem_wait()的非阻塞版本。下面我們逐個介紹和信號量有關(guān)的一些函數(shù)八秃,它們都在頭文件/usr/include/semaphore.h中定義昔驱。
  信號量的數(shù)據(jù)類型為結(jié)構(gòu)sem_t骤肛,它本質(zhì)上是一個長整型的數(shù)腋颠。函數(shù)sem_init()用來初始化一個信號量吓笙。它的原型為:

extern int sem_init __P ((sem_t *__sem, int __pshared, unsigned int __value));

sem為指向信號量結(jié)構(gòu)的一個指針面睛;pshared不為0時(shí)此信號量在進(jìn)程間共享侮穿,否則只能為當(dāng)前進(jìn)程的所有線程共享;value給出了信號量的初始值回铛。
  函數(shù)sem_post( sem_t *sem )用來增加信號量的值茵肃。當(dāng)有線程阻塞在這個信號量上時(shí)袭祟,調(diào)用這個函數(shù)會使其中的一個線程不在阻塞,選擇機(jī)制同樣是由線程的調(diào)度策略決定的您没。
  函數(shù)sem_wait( sem_t *sem )被用來阻塞當(dāng)前線程直到信號量sem的值大于0氨鹏,解除阻塞后將sem的值減一仆抵,表明公共資源經(jīng)使用后減少镣丑。函數(shù)sem_trywait ( sem_t *sem )是函數(shù)sem_wait()的非阻塞版本莺匠,它直接將信號量sem的值減一。
  函數(shù)sem_destroy(sem_t *sem)用來釋放信號量sem辽聊。
  下面我們來看一個使用信號量的例子跟匆。在這個例子中玛臂,一共有4個線程迹冤,其中兩個線程負(fù)責(zé)從文件讀取數(shù)據(jù)到公共的緩沖區(qū)泡徙,另兩個線程從緩沖區(qū)讀取數(shù)據(jù)作不同的處理(加和乘運(yùn)算)堪藐。

/* File sem.c */
#include <stdio.h>
#include <pthread.h>
#include <semaphore.h>
#define MAXSTACK 100
int stack[MAXSTACK][2];
int size=0;
sem_t sem;

/* 從文件1.dat讀取數(shù)據(jù)礁竞,每讀一次模捂,信號量加一*/
void ReadData1(void){
    FILE *fp=fopen("1.dat","r");
    while(!feof(fp)){
        fscanf(fp,"%d %d",&stack[size][0],&stack[size][1]);
        sem_post(&sem);
        ++size;
    }
    fclose(fp);
}

/*從文件2.dat讀取數(shù)據(jù)*/
void ReadData2(void){
    FILE *fp=fopen("2.dat","r");
    while(!feof(fp)){
        fscanf(fp,"%d %d",&stack[size][0],&stack[size][1]);
        sem_post(&sem);
        ++size;
    }
    fclose(fp);
}
/*阻塞等待緩沖區(qū)有數(shù)據(jù)狂男,讀取數(shù)據(jù)后并淋,釋放空間,繼續(xù)等待*/
void HandleData1(void){
    while(1){
        sem_wait(&sem);
        printf("Plus:%d+%d=%d\n",stack[size][0],stack[size][1],
        stack[size][0]+stack[size][1]);
        --size;
    }
}

void HandleData2(void){
    while(1){
        sem_wait(&sem);
        printf("Multiply:%d*%d=%d\n",stack[size][0],stack[size][1],
        stack[size][0]*stack[size][1]);
        --size;
    }
}

int main(void){
    pthread_t t1,t2,t3,t4;
    sem_init(&sem,0,0);
    pthread_create(&t1,NULL,(void *)HandleData1,NULL);
    pthread_create(&t2,NULL,(void *)HandleData2,NULL);
    pthread_create(&t3,NULL,(void *)ReadData1,NULL);
    pthread_create(&t4,NULL,(void *)ReadData2,NULL);
    /* 防止程序過早退出镣典,讓它在此無限期等待*/
    pthread_join(t1,NULL);
}

在Linux下澎剥,我們用命令gcc -lpthread sem.c -o sem生成可執(zhí)行文件sem赶舆。 我們事先編輯好數(shù)據(jù)文件1.dat和2.dat,假設(shè)它們的內(nèi)容分別為1 2 3 4 5 6 7 8 9 10和 -1 -2 -3 -4 -5 -6 -7 -8 -9 -10 叙量,我們運(yùn)行sem绞佩,得到如下的結(jié)果:

  Multiply:-1*-2=2
  Plus:-1+-2=-3
  Multiply:9*10=90
  Plus:-9+-10=-19
  Multiply:-7*-8=56
  Plus:-5+-6=-11
  Multiply:-3*-4=12
  Plus:9+10=19
  Plus:7+8=15
  Plus:5+6=11

從中我們可以看出各個線程間的競爭關(guān)系品山。而數(shù)值并未按我們原先的順序顯示出來這是由于size這個數(shù)值被各個線程任意修改的緣故肘交。這也往往是多線程編程要注意的問題涯呻。

5. 小結(jié)

多線程編程是一個很有意思也很有用的技術(shù)魄懂,使用多線程技術(shù)的網(wǎng)絡(luò)螞蟻是目前最常用的下載工具之一市栗,使用多線程技術(shù)的grep比單線程的grep要快上幾倍填帽,類似的例子還有很多篡腌。希望大家能用多線程技術(shù)寫出高效實(shí)用的好程序來嘹悼。

整理自:http://herowill.blog.sohu.com/104335275.html

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末其监,一起剝皮案震驚了整個濱河市抖苦,隨后出現(xiàn)的幾起案子锌历,更是在濱河造成了極大的恐慌究西,老刑警劉巖怔揩,帶你破解...
    沈念sama閱讀 218,682評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件商膊,死亡現(xiàn)場離奇詭異晕拆,居然都是意外死亡实幕,警方通過查閱死者的電腦和手機(jī)昆庇,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,277評論 3 395
  • 文/潘曉璐 我一進(jìn)店門闸溃,熙熙樓的掌柜王于貴愁眉苦臉地迎上來整吆,“玉大人,你說我怎么就攤上這事辉川”眚” “怎么了?”我有些...
    開封第一講書人閱讀 165,083評論 0 355
  • 文/不壞的土叔 我叫張陵乓旗,是天一觀的道長府蛇。 經(jīng)常有香客問我,道長屿愚,這世上最難降的妖魔是什么汇跨? 我笑而不...
    開封第一講書人閱讀 58,763評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上吸耿,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,785評論 6 392
  • 文/花漫 我一把揭開白布喇肋。 她就那樣靜靜地躺著,像睡著了一般菱鸥。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上茶行,一...
    開封第一講書人閱讀 51,624評論 1 305
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼腔寡。 笑死凭语,一個胖子當(dāng)著我的面吹牛却舀,可吹牛的內(nèi)容都是我干的螃诅。 我是一名探鬼主播袭艺,決...
    沈念sama閱讀 40,358評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼驴党,長吁一口氣:“原來是場噩夢啊……” “哼攘轩!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,261評論 0 276
  • 序言:老撾萬榮一對情侶失蹤腺晾,失蹤者是張志新(化名)和其女友劉穎鼻由,沒想到半個月后蔼紧,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體躏嚎,經(jīng)...
    沈念sama閱讀 45,722評論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,900評論 3 336
  • 正文 我和宋清朗相戀三年仇参,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了罩扇。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片肠鲫。...
    茶點(diǎn)故事閱讀 40,030評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖贱除,靈堂內(nèi)的尸體忽然破棺而出虽缕,到底是詐尸還是另有隱情氮趋,我是刑警寧澤,帶...
    沈念sama閱讀 35,737評論 5 346
  • 正文 年R本政府宣布江耀,位于F島的核電站剩胁,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏祥国。R本人自食惡果不足惜昵观,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,360評論 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望舌稀。 院中可真熱鬧索昂,春花似錦、人聲如沸扩借。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,941評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽潮罪。三九已至,卻和暖如春领斥,著一層夾襖步出監(jiān)牢的瞬間嫉到,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,057評論 1 270
  • 我被黑心中介騙來泰國打工月洛, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留何恶,地道東北人。 一個月前我還...
    沈念sama閱讀 48,237評論 3 371
  • 正文 我出身青樓嚼黔,卻偏偏與公主長得像细层,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子唬涧,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,976評論 2 355

推薦閱讀更多精彩內(nèi)容

  • 多線程系列文章源碼頭文件內(nèi)容: #include #include #include 作為程序員疫赎,就是要減少重復(fù)勞...
    batbattle閱讀 923評論 0 1
  • 簡介 線程創(chuàng)建 線程屬性設(shè)置 線程參數(shù)傳遞 線程優(yōu)先級 線程的數(shù)據(jù)處理 線程的分離狀態(tài) 互斥鎖 信號量 一 線程創(chuàng)...
    第八區(qū)閱讀 8,563評論 1 6
  • 線程基礎(chǔ) 線程是進(jìn)程的一個執(zhí)行單元,執(zhí)行一段程序片段碎节,線程共享全局變量捧搞;線程的查看可以使用命令或者文件來進(jìn)行查看;...
    秋風(fēng)弄影閱讀 738評論 0 0
  • Q:為什么出現(xiàn)多線程? A:為了實(shí)現(xiàn)同時(shí)干多件事的需求(并發(fā))胎撇,同時(shí)進(jìn)行著下載和頁面UI刷新介粘。對于處理器,為每個線...
    幸福相依閱讀 1,582評論 0 2
  • Linux 多線程 - 線程異步與同步機(jī)制 I. 同步機(jī)制 線程間的同步機(jī)制主要包括三個: 互斥鎖:以排他的方式晚树,...
    不愛吃飯的牛牛閱讀 1,398評論 0 1