《UNIX環境高級編程》第11章 線程

11.1 引言

前面討論了進程,學習了UNIX進程的環境、進程間的關係以及控制進程的不同方式。
本章將進一步深入理解進程,瞭解如何使用多個控制線程(線程)在單進程環境中執行多個任務。一個進程中的所有線程都可以訪問該進程的組成部件,如文件描述符內存
不管什麼情況下,只要單個資源需要在多個用戶間共享,就必須處理一致性問題。本章的最後將討論目前可用的同步機制,防止多個線程在共享資源時出現不一致的問題。

11.2 線程概念

典型的UNIX進程可以看成只有一個控制線程:一個進程在某一時刻只能做一個件事情。有了多個控制線程之後,在程序設計時就可以把進程設計成在某一時刻能夠做不止一件事,每個線程處理各自獨立的任務。、

  • 通過爲每種時間類型分配單獨的處理線程,可以簡化異步事件的代碼。
  • 多個進程必須使用操作系統提供的複雜機制才能實現內存和文件描述符的共享。而多個線程自動地可以訪問相同的存儲地址空間和文件描述符。
  • 有些問題可以分解從而提高整個程序的吞吐量。在只有一個控制線程的情況下,一個單線程進程要完成多個任務,只需要把這些任務串行化。但有多個控制線程時,相互獨立的任務的處理就可以交叉進行,此時只需要爲每個任務分配一個單獨的線程。當然只有在兩個任務處理互不依賴的情況下,兩個任務纔可以交叉執行。
  • 交互的程序通常可以通過使用多線程來改善響應時間,多線程可以把程序中處理用戶輸入輸出的部分與其他部分分開。

有些人把多線程的程序設計與多處理器或多核系統聯繫起來。但是即使程序運行在單處理器上,也能得到多線程模型的好處。處理器的數量並不影響程序結構,所以不管處理器的個數多少,程序都可以通過使用線程得以簡化。而且,即使多線程程序咋串行化任務時不得不阻塞,由於某些線程在阻塞的時候還有另外一些線程可以運行,所以多線程程序在單處理器上運行還是可以改善響應時間和吞吐量


每個線程包含表示執行環境所必須的信息,其中包括進程中標識線程的線程ID一組寄存器值調度優先級和策略信號屏蔽字errno變量以及線程私有數據
一個進程的所有信息對該進程的所有線程都是共享的,包括可以執行程序的代碼、程序的全局內存堆內存以及文件描述符


我們將要討論的線程接口來自POSIX.1。線程接口也稱爲“pthread”或“POSIX 線程”。
POSIX 線程的功能測試宏是_POSIX_THREADS,應用程序可以把這個宏用於#ifdef測試,從而在編譯時確定是否支持線程
也可以把常數_SC_THREADS用於調用sysconf函數,進而在運行時確定是否支持線程

11.3 線程標識

就像每個進程都有一個進程ID一樣,每個線程也有一個線程ID。進程ID在整個系統中是唯一的,但線程ID不同,線程ID只有在它所屬的進程上下文中才有意義。
線程ID用pthread_t數據類型來表示,實現的時候可以用一個結構來代表pthread_t數據類型,所有可移植的操作系統實現不能把它當做整數處理。因此必須使用一個函數對兩個線程ID進程比較。

#include <pthread.h>
int pthread_equal(pthread_t tid1,pthread_t tid2);

使用結構表示pthread_t數據類型的後果是不能使用一種可移植的方式打印該數據類型的值。


線程可以使用pthread_self函數獲得自身的線程ID。

#include <pthread.h>
pthread_t pthread_self(void);

當線程需要識別以線程ID作爲標識的數據結構時,pthread_self函數可以與pthread_equal函數一起使用。例如:
主線程可能把工作任務放在一個隊列中,用線程ID來控制每個工作線程處理哪些作業。如下圖,主線程把新的作業放到一個工作隊列中,由3個線程組成的線程池從隊列中移除作業。主線程不允許每個線程任意處理從隊列頂端取出的作業,而是由主線程控制作業的分配,主線程會在每個待處理作業的結構中放置處理該作業的線程ID,每個工作線程只能移除標有自己線程ID的作業。這裏寫圖片描述

11.4 線程創建

在傳統UNIX進程模型中,每個進程只有一個控制線程。從概念上講,這與基於線程的模型中每個進程只包含一個線程是相同的。
在POSIX線程(pthread)的情況下,程序開始運行時,它也是以單進程中的單個控制線程啓動的。在創建多個控制線程以前,程序的行爲與傳統的進程並沒有什麼區別。新增的線程可以通過調用pthread_create函數創建。

#include <pthread.h>
int pthread_create(pthread_t *restrict tidp,const pthread_attr_t *restrict attr,void *(*start_rtn)(void *),void *restrict arg);
  • 參數tidp所指向的內存單元存儲了新線程的線程ID;
  • 參數attr用於定製不同的線程屬性。可以設置爲NULL,創建一個具有默認屬性的線程。
  • 新創建的線程從start_rtn函數的地址開始運行,該函數只有一個無類型參數指針arg,如果需要向start_rtn函數傳遞的參數有一個以上,那麼需要把這些參數放到一個結構中,然後把這個結構的地址作爲arg參數傳入。

線程創建時並不能保證哪個線程會先運行:是新創建的線程,還是調用線程。新創建的線程可以訪問進程的地址空間,並且繼承了調用線程的浮點環境信號屏蔽字,但是該線程的掛起信號集會被清除


pthread函數在調用失敗時會返回錯誤碼,它們並不像其他POSIX函數一樣設置errno。每個線程都提供errno的副本,這只是爲了與使用errno的現有函數兼容。


#include <stdio.h>
#include <stdlib.h>

#include <pthread.h>

void printSysInfo(char *);
void *newThread(void *);


void printSysInfo(char *str)
{
    pid_t pid;
    pthread_t tid;

    pid=getpid();
    tid=pthread_self();

    printf("%s :pid is %ld,tid is %ld.\n",str,pid,tid);
}

void *newThread(void *arg)
{


    printf("the argument is %s. \n",(char *)arg);

    printSysInfo("in new thread");

    return((void *)0);

}


int main(void)
{
    pthread_t tid;
    int err;
    char *str="hello!";
    err=pthread_create(&tid,NULL,newThread,str);
    printSysInfo("in main thread");
    sleep(1);
    return 0;
}

這裏寫圖片描述

11.5 線程終止

如果進程中的任意線程調用了exit_Exit或者_exit,那麼整個進程就會終止。如果默認動作是終止的信號發送到線程,那麼發送到線程的信號就會終止整個進程(12.8節再討論信號與線程時如何交互的)。
單個線程可以通過3種方式退出,因此可以在不終止整個進程的情況下,停止它的控制流。

  1. 線程可以簡單地從啓動例程中返回,返回值是線程的退出碼。
  2. 線程可以被同一進程中的其他線程取消。
  3. 線程調用pthread_exit。
#include <pthread.h>
void pthread_exit(void *rval_ptr);

rval_ptr參數是一個無類型指針,與傳遞給啓動例程的單個參數類似。進程中的其他線程也可以通過調用pthread_join函數訪問到這個指針。

#include <pthread.h>
int pthread_join(pthread_t thread,void **rval_ptr);

調用線程將一直阻塞,直到指定的線程調用pthread_exit、從啓動例程返回或者被取消。如果線程簡單地從啓動例程中返回,rval_ptr就包含返回碼。如果線程被(其他線程)取消,rval_ptr指定的內存單元被設置爲PTHREAD_CANCELED
如果對線程的返回值並不感興趣,那麼可以把rval_ptr設置爲NULL。這種情況下,調用pthread_join函數可以等待指定線程終止,但並不獲取線程的終止狀態。

#include <stdio.h>
#include <stdlib.h>

#include <pthread.h>

void *newThread1(void *);
void *newThread2(void *);



void *newThread1(void *arg)
{


    printf("in thread 1. \n");


    return((void *)1);  //情況1.正常返回

}

void *newThread2(void *arg)
{

    printf("in thread 2. \n");

    pthread_exit((void *)3);  //情況2.pthread_exit返回
    //addr 0x03

}



int main(void)
{
    pthread_t tid1,tid2;
    int err;
    int *retNum;  //用於接收return或pthread_exit返回指針的指針變量

    err= pthread_create(&tid1,NULL,newThread1,NULL);
    err= pthread_create(&tid2,NULL,newThread2,NULL);

    pthread_join(tid1,&retNum);  //二重指針是提供用於接收返回指針的指針變量的地址
    printf("thread1 return %d \n",retNum);

    pthread_join(tid2,&retNum);
    printf("thread2 return %d \n",retNum);

    sleep(1);
    return 0;
}

這裏寫圖片描述

一個C語言的基本概念:函數不能返回棧上變量的地址。自動變量內存地址中的值在函數退出後是不確定的。尤其在上面這些返回指針的地方。要不然返回只讀數據區中的常量,要不然就是malloc堆中的動態變量,要不然就是全局變量。


線程可以通過調用pthread_cancel函數來請求取消同一進程中的其他線程。

#include <pthread.h>
int pthread_cancel(pthread_t tid);

在默認情況下,pthread_cancel函數會使得由tid標識的線程的行爲表現爲如同調用了參數爲PTHREAD_CANCELED的pthreas_exit函數。但是線程可以忽略取消或者控制如何被取消(在後面12章討論)。注意:pthread_cancel並不等待線程終止,它僅僅提出請求。
線程可以安排它退出時需要調用的函數,這與進程在退出時可以調用atexit函數安排退出時類似的。這樣的函數稱爲線程清理處理程序(thread cleanup handler)。一個線程可以建立多個清理處理程序。處理程序註冊在棧中,也就是說,它們執行的順序與它們註冊的順序相反。

#include <pthread.h>
void pthread_cleanup_push(void (*rtn)(void *),void *arg);
void pthread_cleanup_pop(int execute);

當線程執行以下動作時,清理函數rtn是由pthread_cleanip_push函數調度的,調用時只有一個參數arg

  1. 調用pthread_exit時;
  2. 響應取消請求時;
  3. 用非零execute參數調用pthread_cleanup_pop時。
  4. 注意!!線程從啓動例程返回(正常退出)並不會調用註冊的清理程序。
    如果execute參數設置爲0,清理函數將不被調用。不管發生上述哪種情況,pthread_cleanup_pop都將刪除上次pthread_cleanip_push調用建立的清理處理程序。
    這些函數有一個限制,由於它們可以實現爲宏,所以必須在線程相同的作用域中以匹配對的形式使用。因爲pthread_cleanip_push包含了’{‘字符,pthread_cleanup_pop包含了’}’字符.
#include <stdio.h>
#include <pthread.h>


void *fun1(void *arg);
void *fun2(void *arg);

void cleanup(void *arg);


void *fun1(void *arg)
{
    printf("thread1 printed the message.\n");

    pthread_cleanup_push(cleanup,"thread 1 first handler");
    pthread_cleanup_push(cleanup,"thread 1 second handler");

    if(arg)
        return((void *)1);
    pthread_cleanup_pop(0);
    pthread_cleanup_pop(0);

    return((void *)1);
}

void *fun2(void *arg)
{
    printf("thread2 printed the message.\n");

    pthread_cleanup_push(cleanup,"thread 2 first handler");
    pthread_cleanup_push(cleanup,"thread 2 second handler");

    if(arg)
        pthread_exit((void *)2);
    pthread_cleanup_pop(0);
    pthread_cleanup_pop(0);

    pthread_exit((void *)2);
}

void cleanup(void *arg)
{
    printf("%s. \n",arg);
}




int main(void)
{
    pthread_t tid1,tid2;
    void *tret;

    pthread_create(&tid1,NULL,fun1,1);
    pthread_create(&tid2,NULL,fun2,1);

    pthread_join(tid1,&tret);
    printf("thread1 return %d. \n",tret);
    pthread_join(tid2,&tret);
    printf("thread2 return %d \n",tret);

    return 0;
}

這裏寫圖片描述


線程函數與進程函數之間的相似處:

進程原語 線程原語 描述
fork pthread_create 創建新的控制流
exit pthread_exit 從現有的控制流中退出
waitpid pthread_join 從控制流中得到退出狀態
atexit pthread_cleanup_push 註冊在退出控制流時調用的函數
getpid pthread_self 獲得控制流ID
abort pthread_calcel 請求控制流的非正常退出

在默認情況下,線程的終止狀態保存直到對該線程調用pthread_join。如果線程已經被分離,線程的底層存儲資源可以在線程終止時立即被收回。在線程被分離後,我們不能調用pthread_join 函數等待它的終止狀態,因爲對分離狀態的線程調用pthread_join會產生未定義行爲。
可以調用pthread_detach分離線程。

#include <pthread.h>
int pthread_detach(pthread_t tid);

11.6 線程同步

當多個控制線程共享相同的內存時,需要確保每個線程看到一致的數據視圖。如果每個線程使用的變量都是其他線程不會讀取和修改的,那麼就不存在一致性問題。同樣,如果變量是制度的,多個線程同時讀取該變量也不會有一致性問題。但是,當一個線程可以修改的變量,其他線程也可以讀取或修改的時候,我們就需要對這些線程進行同步,確保它們在訪問變量的存儲內容時不會訪問到無效的值。
當一個線程修改變量時,其他線程在讀取或修改這個變量時可能會看到一個不一致的值。
爲了解決這個問題,線程不得不使用鎖,同一時間只允許一個線程訪問該變量。

11.6.1 互斥量mutex

可以使用pthread的互斥接口來保護數據,確保同一時間只有一個線程訪問數據。互斥量(mutex)從本質上說是一把,在訪問共享資源前對互斥量進行設置(加鎖),在訪問完成後釋放(解鎖)互斥量。對互斥量進行加鎖以後任何其他試圖再次對互斥量加鎖的線程都會被阻塞直到當前線程釋放該互斥鎖
如果釋放互斥量時有一個以上的線程阻塞,那麼所有該鎖上的阻塞線程都會變成可運行狀態,第一個變爲可運行的線程就可以對互斥量加鎖,其他線程就會看到互斥量依然是鎖着的,只能回去再次等待它重新變爲可用。在這種方式下,每次只有一個線程可以向前執行。
互斥變量是用pthread_mutex_t數據類型表示的。在使用互斥變量以前,必須首先對它進行初始化,可以把它設置爲常量PTHREAD_MUTEX_INITIALIZER(只適用於靜態分配的互斥量),也可以通過調用pthread_mutex_init函數進行初始化。如果動態分配互斥量(例如,通過調用malloc函數),在釋放內存前需要調用pthread_mutex_destroy

#include<pthread.h>
int pthread_mutex_init(pthread_mutex_t *restrict mutex,const pthread_mutexattr_t *restrict attr);    //默認屬性爲NULL
int pthread_mutex_dstroy(pthread_mutex_t *mutex);

對互斥量進行加鎖,需要調用pthread_mutex_lock。如果互斥量已經上鎖,調用線程將阻塞直到互斥量被解鎖。對互斥量解鎖需要調用pthread_mutex_unlock

#include<pthread.h>
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_trylock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);

如果線程不希望被阻塞,它可以使用pthread_mutex_trylock嘗試對互斥量進行加鎖。如果調用pthread_mutex_trylock時互斥量處於未鎖住狀態,那麼pthread_mutex_trylock將鎖住互斥量,不會出現阻塞直接返回0,否則就會失敗,不能鎖住互斥量,返回EBUSY

11.6.2 避免死鎖

如果線程試圖對同一個互斥量加鎖兩次,那麼它自身就會陷入死鎖狀態,但是使用互斥量時,還有其他不太明顯的方式也能產生死鎖。例如程序中使用一個以上的互斥量時,如果允許一個線程一直佔有第一個互斥量,並且在試圖鎖住第二個互斥量時處於阻塞狀態,但是擁有第二個互斥量的線程也在試圖鎖住第一個互斥量。因爲兩個線程都在相互請求另一個線程擁有的資源,所以這兩個線程都無法向前運行,於是就產生死鎖。

11.6.3 函數pthread_mutex_timedlock

當線程試圖獲取一個已經加鎖的互斥量時,pthread_mutex_timedlock互斥量原語允許綁定線程阻塞時間。pthread_mutex_timedlock函數與pthread_mutex_lock函數基本上是等價的,但是在達到超時時間值時,pthread_mutex_timedlock不會對互斥量進行加鎖,而是返回錯誤碼ETIMEDOUT

#include <pthread.h>
#include <time.h>
int pthread_mutex_timedlock(pthread_mutex_t *restrict mutex,const struct timespec *restrict tsptr);

超時指定願意等待的絕對時間(注意是絕對時間)。這個超時時間是用timespec結構來表示的,它用秒和納秒來描述時間。
以下的例子給出瞭如何用pthread_mutex_timedlock避免永久阻塞。

#include <stdio.h>
#include <pthread.h>
#include <time.h>
#include <error.h>
#include <errno.h>
#include <err.h>
#include <string.h>



int main(void)
{
    int err;
    struct timespec tout;
    struct tm *tmp;
    char buf[64];
    pthread_mutex_t lock=PTHREAD_MUTEX_INITIALIZER;

    pthread_mutex_lock(&lock);
    printf("mutex is locked. \n");

    clock_gettime(CLOCK_REALTIME,&tout);
    tmp=localtime(&tout.tv_sec);
    strftime(buf,sizeof(buf),"%r",tmp);
    printf("current time is %s \n",buf);
    tout.tv_sec+=10;

    err=pthread_mutex_timedlock(&lock,&tout);

    clock_gettime(CLOCK_REALTIME,&tout);
    tmp=localtime(&tout.tv_sec);
    strftime(buf,sizeof(buf),"%r",tmp);
    printf("current time is %s \n",buf);

    if(err==0)
        printf("mutex locked again! \n");
    else
        printf("can not lock  mutex again:%s \n",strerror(err));

    return 0;
}

這裏寫圖片描述

11.6.4 讀寫鎖

讀寫鎖(read-write-lock)與互斥量類似,不過讀寫鎖允許更高的並行性。互斥量要麼是鎖住狀態,要麼就是不加鎖狀態,而且一次只有一個線程可以對其加鎖。
讀寫鎖有3種狀態:讀模式下加鎖狀態寫模式下加鎖狀態不加鎖狀態。一次只有一個線程可以佔有寫模式的讀寫鎖,但是多個線程可以同時佔有讀模式的讀寫鎖。

  • 讀寫鎖是寫加鎖狀態時,在這個鎖被解鎖之前,所有試圖對這個鎖加鎖的線程都會被阻塞。
  • 讀寫鎖是讀加鎖狀態時,所有試圖以讀模式對它進行加鎖的線程都可以得到訪問權,但是任何希望以寫模式對此鎖進行加鎖的線程都會阻塞,直到所有線程釋放它們的讀鎖爲止
  • 當讀寫鎖處於讀模式鎖住狀態時,有一個線程試圖以寫模式獲取鎖時,讀寫鎖通常會阻塞隨後的讀模式鎖請求。這樣可以避免讀模式鎖長期佔有,而等待的寫模式鎖請求一直得不到滿足。

讀寫鎖非常適合於對數據結構讀的次數遠大於寫的情況。當讀寫鎖在寫模式下時,他所保護的數據結構就可以安全地修改,因爲一次只有一個線程可以在寫模式下擁有這個鎖。當讀寫鎖在讀模式下時,只要線程先獲取了讀模式下的讀寫鎖,該鎖保護的數據就可以被多個獲得讀模式的線程讀取。
讀寫鎖也叫共享互斥鎖(shared-exclisive lock)。當讀寫鎖是讀模式鎖住時,就可以說成是共享模式鎖住的。當它是寫模式鎖住時就可以說成是互斥模式鎖住的
與互斥量相比,讀寫鎖在使用之前必須初始化,在釋放它們的底層內存之前必須銷燬。

#include<pthread.h>
int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock,const pthread_rwlockattr_t *restrict attr);    //默認屬性爲NULL
int pthread_rwlock_dstroy(pthread_rwlock_t *rwlock);

SUS 在XSI擴展中定義了PTHREAD_RWLOCK_INITIALIZER常量。如果默認屬性就足夠的話,可以用它對靜態分配的讀寫鎖進行初始化。

#include<pthread.h>
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);//讀模式鎖定
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);//寫模式鎖定
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);//解鎖

各種系統實現可能會對共享模式的讀寫鎖有次數限制,所以要檢查pthread_rwlock_rdlock的返回值進行檢查。

#include<pthread.h>
int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);
//可以獲取鎖時,這兩個函數返回0,否則他們返回錯誤EBUSY。

11.6.5帶有超時的讀寫鎖

與互斥量一樣,SUS提供了帶有超時的讀寫鎖加鎖函數,使得應用程序在獲取讀寫鎖時避免陷入永久阻塞狀態。

#include <pthread.h>
#include <time.h>
int pthread_rwlock_timedrdlock(pthread_rwlock_t *restrict rwlock,const struct timespec *restrict tsptr);
int pthread_rwlock_timedwrlock(pthread_rwlock_t *restrict rwlock,const struct timespec *restrict tsptr);

與pthreas_mutex_timedlock函數類似,超時時間是絕對時間,而不是相對時間。

11.6.6 條件變量

條件變量是線程可用的另一種同步機制。條件變量給多個線程提供了一個會合的場所。條件變量與互斥量一起使用時,允許線程以無競爭的方式等待特定的條件發生。
條件本身是由互斥量保護的。線程在改變條件狀態之前必須首先鎖住互斥量。其他線程在獲得互斥量之前不會察覺到這種改變,因爲互斥量必須在鎖定以後才能計算條件。
在使用條件變量之前,必須先對它進行初始化。由pthread_cond_t數據類型表示的條件變量可以用兩種方式進行初始化,可以把常量PTHREAD_COND_INITIALIZER賦給靜態分配的條件變量,但是如果條件變量是動態分配的,則需要使用pthread_cond_init函數對它進行初始化。
在釋放條件變量底層的內存空間之前,可以使用pthread_cond_destroy函數對條件變量進行反初始化(deinitialize)。

#include <pthread.h>
#include <time.h>
int pthread_cond_init(pthread_cond_t *restrict cond,const pthread_cond_t *restrict attr);//屬性可設爲NULL
int pthread_cond_destroy(pthread_cond_t *cond);

我們使用pthread_cond_wait等待條件變量變爲真。如果在給定的時間內條件不能滿足,那麼會生成一個返回錯誤碼的變量。

#include <pthread.h>
int pthread_cond_wait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex);
int pthread_cond_timewait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex,const struct timespec *restrict tsptr);

傳遞給pthread_cond_wait的互斥量對條件進行保護。

!!!!!!!!!!!!以下很重要!!!!!!!!!!!

調用者把鎖住的互斥量(先外部上鎖)傳遞給函數,函數然後自動把調用線程放到等待條件的線程表上,對互斥量解鎖(再內部解鎖)這就關閉了條件檢查和線程進入休眠狀態等待條件改變這兩個操作之間的時間通道(在條件檢查和等待條件時不會錯過信號,因爲互斥量加鎖了,所以信號發佈出來),這樣線程就不會錯過條件的任何變化。 解鎖之後函數就處於等待信號的狀態,當信號到達時,pthread_cond_wait函數返回,互斥量再次被鎖住(內部加鎖),之後留給外部解鎖。

#include <pthread>

struct msg{
struct msg *m_next;
};

struct msg *workg;

pthread_cont_t qready = PTHREAD_COND_INITIALIZER;
pthread_mutex_t qlock = PTHREAD_MUTEX_INITIALIZER;

void process_msg(void)
{
struct msg *mp;

for(;;)
{
pthread_mutex_lock(&qlock);
while(workq == NULL)
pthread_cond_wait(&qready,&qlock);
//線程在此阻塞,將線程保存到線程表後釋放鎖,開始等待條件變量;函數收到信號後返回,並加鎖。
//收到信號後,此時workq已經不是NULL,所以繼續向下執行。
mp = workq;
workq = mp->m_next;
pthread_mutex_unlock(&qlock);//這裏對互斥鎖解鎖,表明同步完成。
}
}

void enqueue_msg(struct msg *mp)
{
pthread_mutex_lock(&qlock);//
mp->m_next = workq;
workq = mp;
pthread_mutex_unlock(&qlock);
pthread_cond_signal(&qready);//此處發送信號。
}

這裏講的比較清楚:
http://blog.csdn.net/hiflower/article/details/2195350
條件變量要和互斥量相聯結,以避免出現條件競爭一個線程預備等待一個條件變量,當它在真正進入等待之前,另一個線程恰好觸發了該條件。

11.6.7自旋鎖

自旋鎖與互斥量類似,但它不是通過休眠使進程阻塞,而是在獲取鎖之前一直處於忙等(自旋)阻塞狀態。自旋鎖可以用於以下情況:鎖被持有的時間短,而且線程並不希望在重新調度上(不產生重新調度)花費太多的成本。
由於線程自旋等待的時候CPU不能做其他事情,因此自旋鎖只能被持有一小段時間。
自旋鎖用在非搶佔式內核()中時是非常有用的:除了提供互斥機制以外,它們會阻塞中斷,這樣中斷處理程序就不會讓系統陷入死鎖狀態,因爲它需要獲取已被加鎖的自旋鎖(把中斷想成另一種搶佔)。在這種類型的內核中,中斷處理程序不能休眠(因爲非搶佔式內核不能休眠,因此不能使用互斥鎖、讀寫鎖、條件變量。),因此它們能用的同步原語只能是自旋鎖。
很多互斥量的實現非常高效,以至於應用程序採用互斥鎖的性能與曾經採用自旋鎖的性能基本上是相同的。事實上,有些互斥量的實現在試圖獲取互斥量的時候會自旋一小段時間,只有在自旋計數達到某一閾值時纔會休眠。這些因素再加上現代CPU的進步,使得上下文切換越來越快,也是的自旋鎖在某些特定的情況下才有用。


自旋鎖的接口與互斥量的接口類似,這使得它可以比較容易的從一個替換爲另一個。

#include<pthread.h>
int pthread_spin_init(pthread_spin_t *lock,int pshared); 
int pthread_spin_dstroy(pthread_spin_t *lock);

pshared參數表示進程共享屬性,表明自旋是如何獲取的。只有在支持線程進程共享同步選項的平臺上纔有效。

  • 設爲PTHREAD_PROCESS_SHARED,則自旋鎖能被底層內存的所有線程獲取,即使那些線程屬於不同的進程。
  • 設爲PTHREAD_PROCESS_private,則自旋鎖就只能被初始化該鎖的進程內部的線程所訪問。
#include<pthread.h>
int pthread_spin_lock(pthread_spin_t *lock);
int pthread_spin_trylock(pthread_spin_t *lock);
int pthread_spin_unlock(pthread_spin_t *lock);

需要注意,不要調用在持有自旋鎖的情況下可能會進入休眠狀態的函數。如果調用了這些函數,會浪費CPU資源,因爲其他線程需要獲取自旋鎖需要的時間就延長了。

11.6.8 屏障

屏障(barrier)是用戶協調多個線程並行工作的同步機制。屏障允許每個線程等待,知道所有的合作線程都達到某一點,然後從該點繼續執行。我們已經看到一種屏障pthread_join函數就是一種屏障,允許一個線程等待,知道另一個線程退出。
但屏障的概念更廣,它允許任意數量的線程等待,直到所有的線程完成處理工作,而且線程不需要退出。所有線程達到屏障後可以接着工作。
使用pthread_barrier_init函數對屏障進行初始化,用pthread_barrier_destroy函數進行反初始化。

#include<pthread.h>
int pthread_barrier_init(pthread_barrier_t *restrict barrier,const pthread_barrierattr_t *restrict attr,unsigned int count); 
int pthread_barrier_dstroy(pthread_barrier_t *barrier);
  • const參數指定,在允許所有線程繼續運行之前,必須達到屏障的線程數目。
  • attr參數指定屏障對象的屬性
    可以使用pthread_barrier_wait 函數來表明,線程已經完成工作,準備等待所有其他線程趕上來。
#include<pthread.h>

int pthread_barrier_wait(pthread_barrier_t *barrier);

調用pthread_barrier_wait的線程在屏障計數未滿足條件時,會進入休眠狀態。如果該線程是最後一個調用pthread_barrier_wait的線程,就滿足了屏障計數,所有的線程都被喚醒。

11.7 小結

本章介紹了線程的概念;
討論了現有的創建和銷燬線程的POSIX.1原語;
介紹了線程同步問題,討論了5個基本的同步機制(互斥量、讀寫鎖、條件變量、自旋鎖以及屏障)。瞭解瞭如何使用它們來保護共享資源。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章