就像線程具有屬性一樣,線程的同步對象(如互斥量、讀寫鎖、條件變量、自旋鎖和屏障)也有屬性。http://www.CUOXin.com/nufangrensheng/p/3521654.html中介紹了自旋鎖的唯一的一個屬性,本篇介紹互斥量、讀寫鎖、條件變量及屏障的屬性。
1、互斥量屬性
用pthread_mutexattr_init初始化pthread_mutexattr_t結構,用pthread_mutexattr_destroy來對該結構進行回收。
#include <pthread.h>int pthread_mutexattr_init( pthread_mutexattr_t *attr );int pthread_mutexattr_destroy( pthread_mutexattr_t *attr );返回值:若成功則返回0,否則返回錯誤編號
pthread_mutexattr_init函數用默認的互斥量屬性初始化pthread_mutexattr_t結構。值得注意的兩個屬性是進程共享屬性和類型屬性。POSIX.1中,進程共享屬性是可選的,可以通過檢查系統中是否定義了_POSIX_THREAD_PROCESS_SHARED符號來判斷這個平臺是否支持進程共享這個屬性,也可以在運行時把_SC_THREAD_PROCESS_SHARED參數傳給sysconf函數進行檢查。雖然這個選項并不是遵循POSIX標準的操作系統必須提供的,但是Single UNIX Specification要求遵循XSI標準的操作系統支持這個選項。
在進程中,多個線程可以訪問同一個同步對象,這是默認的行為。在這種情況下,進程共享互斥量屬性需要設置為PTHREAD_PROCESS_PRIVATE。
存在這樣一種機制:允許相互獨立的多個進程把同一個內存區域映射到它們各自獨立的地址空間中。就像多個線程訪問共享數據一樣,多個進程訪問共享數據通常也需要同步。如果進程共享互斥量屬性設置為PTHREAD_PROCESS_SHARED,從多個進程共享的內存區域中分配的互斥量就可以用于這些進程的同步。
可以使用pthread_mutexattr_getpshared函數查詢pthread_mutexattr_t結構,得到它的進程共享屬性;可以用pthread_mutexattr_setpshared函數修改進程共享屬性。
#include <pthread.h>int pthread_mutexattr_getpshared( const pthread_mutexattr_t *restrict attr, int *restrict pshared );int pthread_mutexattr_setpshared( pthread_mutexattr_t *attr, int pshared );返回值:若成功則返回0,否則返回錯誤編號
進程共享互斥量屬性設為PTHREAD_PROCESS_PRIVATE時,允許pthread線程庫提供更加有效的互斥量實現,這在多線程應用程序中是默認的情況。在多個進程共享多個互斥量的情況下,pthread線程庫可以限制開銷較大的互斥量實現。
類型互斥量屬性控制著互斥量的特性。POSIX.1定義了四種類型。PTHREAD_MUTEX_NORMAL類型是標準的互斥量類型,并不做任何特殊的錯誤檢查或死鎖檢測。PTHREAD_MUTEX_ERRORCHECK互斥量類型提供錯誤檢查。
PTHREAD_MUTEX_RECURSIVE互斥量類型允許同一線程在互斥量解鎖之前對該互斥量進行多次加鎖。用一個遞歸互斥量維護鎖的計數。在解鎖的次數和加鎖的次數不相同的情況下不會釋放鎖。所以如果對一個遞歸互斥量加鎖兩次,然后對它解鎖一次,這個互斥量依然處于加鎖狀態,在對它再次解鎖以前不能釋放該鎖。
最后,PTHREAD_MUTEX_DEFAULT類型可以用于請求默認語義。操作系統在實現它的時候可以把這種類型自由地映射到其他類型。例如,在linux中,這種類型映射為普通的互斥量類型。
四種類型的行為如表12-4所示?!安徽加脮r解鎖”這一欄指的是一個線程對被另一個線程加鎖的互斥量進行解鎖的情況;“在已解鎖時解鎖”這一欄指的是當一個線程對已經解鎖的互斥量進行解鎖時將會發生的情況,這通常是編碼錯誤所致。
表12-4 互斥量類型行為
可以用pthread_mutexattr_gettype函數得到互斥量類型屬性,用pthread_mutexattr_settype函數修改互斥量類型屬性。
#include <pthread.h>int pthread_mutexattr_gettype( const pthread_mutexattr_t * restrict attr, int *restrict type );int pthread_mutexattr_settype( pthread_mutexattr_t *attr, int type );兩者的返回值都是:若成功則返回0,否則返回錯誤編號
http://www.CUOXin.com/nufangrensheng/p/3521654.html中曾介紹過,互斥量可用于保護與條件變量關聯的條件。在阻塞線程之前,pthread_cond_wait和pthread_cond_timedwait函數釋放與條件相關的互斥量,這就允許其他線程獲取互斥量、改變條件、釋放互斥量并向條件變量發送信號。既然改變條件時必須占有互斥量,所以使用遞歸互斥量并不是好的辦法。如果遞歸互斥量被多次加鎖,然后用在調用pthread_cond_wait函數中,那么條件永遠都不會得到滿足,因為pthread_cond_wait所作的解鎖操作并不能釋放互斥量。
如果需要把現有的單線程接口放到多線程環境中,遞歸互斥量是非常有用的,但由于程序兼容性的限制,不能對函數接口進行修改。然而由于遞歸鎖的使用需要一定技巧,它只應在沒有其他可行方案的情況下使用。
實例
使用遞歸鎖的情況:
main(){ func1(x); ...... func2(x);}func1(){ pthread_mutex_lock(x->lock); ...... func2(x); ...... pthread_mutex_unlock(x->lock);}func2(){ pthread_mutex_lock(x->lock); ...... pthread_mutex_unlock(x->lock);}
上面的代碼解釋了遞歸鎖看似解決并發問題的情況。假設func1和func2是函數庫中現有的函數,其接口不能改變,因為存在調用這兩個接口的應用程序,而且應用程序不能改動。
為了保持接口跟原來相同,可以把互斥量嵌入到數據結構中,把這個數據結構的地址(x)作為參數傳入。這種方案只有在為該數據結構提供了分配函數時才可行,所以應用并不知道數據結構的大小(假設在其中增加互斥量后必須擴大該數據結構的大?。?/p>
如果在最初定義數據結構時,預留了足夠的可填充字段,允許把一些填充字段替換成互斥量,那么這種方法也是可行的。不過,大多數程序員并不善于預測未來,所以這不是普遍可行的經驗。
如果func1和func2函數都必須操作這個結構,而且可能會有多個線程同時訪問該數據結構,那么func1和func2必須在操作數據以前對互斥量加鎖。當func1必須調用func2時,如果互斥量不是遞歸類型,那么就會出現死鎖。如果能在調用func2之前釋放互斥量,在func2返回后重新獲取互斥量,那么就可以避免使用遞歸互斥量,但這也給其他的線程提供了機會,其他的線程可能在func1執行期間得到互斥量的控制權,修改這個數據結構。
避免使用遞歸鎖的情況:
main(){ func1(x); ...... func2(x);}func1(){ pthread_mutex_lock(x->lock); ...... func2_locked(x); ...... pthread_mutex_unlock(x->lock);}func2(){ pthread_mutex_lock(x->lock); func2_locked(x); pthread_mutex_unlock(x->lock);}
上面的代碼顯示了這種情況下使用遞歸互斥量的另一種替代方法。通過提供func2函數的私有版本(稱之為func2_locked函數),可以保持func1和func2函數接口不變,并且避免使用遞歸互斥量。要調用func2_locked函數,必須占有嵌入到數據結構中的互斥量,這個數據結構的地址是作為參數傳入的。func2_locked的函數體包含func2的副本(原來的、沒有加入互斥量的func2),func2現在只是用以獲取互斥量,調用func2_locked,最后釋放互斥量。
如果并不一定要保持庫函數接口不變,就可以在每個函數中另外再加一個參數,以表明這個結構是否被調用者鎖定。但是,如果可能的話,保持接口不變通常是更好的選擇,這樣可以避免實現過程中人為加入的東西對原有接口產生不良影響。
提供函數的加鎖版本和不加鎖版本,這樣的策略在簡單的情況下通常是可行的。在比較復雜的情況下,例如庫需要調用庫以外的函數,而且可能會再次回調庫中的函數時,就需要依賴遞歸鎖。
實例
程序清單12-2解釋了有必要使用遞歸互斥量的另一種情況。這里,有一個“超時”(timeout)函數,它允許另一個函數可以安排在未來的某個時間運行。假設線程并不是很昂貴的資源,可以為每個未決的超時函數創建一個線程。線程在時間未到時將一直等待,時間到了以后就調用請求的函數。
#include "apue.h"#include <pthread.h>#include <time.h>#include <sys/time.h>extern int makethread(void *(*)(void *), void *);struct to_info{ void (*to_fn)(void *); /* function */ void *to_arg; /* argument */ struct timespec to_wait; /* time to wait */};#define SECTONSEC 1000000000 /* seconds to nanoseconds */#define USECTONSEC 1000 /* microseconds to nanoseconds */void *timeout_helper(void *arg){ struct to_info *tip; tip = (struct to_info *)arg; nanosleep(&tip->to_wait, NULL); (*tip->to_fn)(tip->to_arg); return(0);}void timeout(const struct timespec *when, void (*func)(void *), void *arg){ struct timespec now; struct timeval tv; struct to_info *tip; int err; gettimeofday(&tv, NULL); now.tv_sec = tv.tv_sec; now.tv_nsec = tv.tv_usec * USECTONSEC; if((when->tv_sec > now.tv_sec) || (when->tv_sec == now.tv_sec && when->tv_nsec > now.tv_nsec)) { tip = malloc(sizeof(struct to_info)); if(tip != NULL) { tip->to_fn = func; tip->to_arg = arg; tip->to_wait.tv_sec = when->tv_sec - now.tv_sec; if(when->tv_nsec >= now.tv_nsec) { tip->to_wait.tv_nsec = when->tv_nsec - now.tv_nsec; } else { tip->to_wait.tv_sec--; tip->to_wait.tv_nsec = SECTONSEC - now.tv_nsec + when->tv_nsec; } err = makethread(timeout_helper, (void *)tip); if(err == 0) return; } } /* * We get here if (a) when <= now, or (b) malloc fails, or * (c) we can't make a thread, so we just call the function now. */ (*func)(arg);}pthread_mutexattr_t attr;pthread_mutex_t mutex;void retry(void *arg){ pthread_mutex_lock(&mutex); /* perform retry steps ... */ pthread_mutex_unlock(&mutex);}int main(void){ int err, condition, arg; struct timespec when; if((err == pthread_mutexattr_init(&attr)) != 0) err_exit(err, "pthread_mutexattr_init failed"); if((err == pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE)) != 0) err_exit(err, "can't set recursive type"); if((err == pthread_mutex_init(&mutex, &attr)) != 0) err_exit(err, "can't create recursive mutex"); /* ... */ pthread_mutex_lock(&mutex); /* ... */ if(condition) { /* calculate target time "when" */ timeout(&when, retry, (void *)arg); } /* ... */ pthread_mutex_unlock(&mutex); /* ... */ exit(0);}
如果不能創建線程,或者安排函數運行的時間已過,問題就出現了。在這種情況下,要從當前環境中調用之前請求運行的函數,因為函數要獲取的鎖和現在占有的鎖是同一個,除非該鎖是遞歸的,否則就會出現死鎖。
這里使用程序清單12-1中的makethread函數以分離狀態創建線程。希望函數在未來的某個時間運行,而且不希望一直等待線程結束。
可以調用sleep等待超時到達,但它提供的時間粒度是秒級的,如果希望等待的時間不是整數秒,需要用nanosleep(2)函數,它提供了類似的功能。
timeout的調用者需要占有互斥量來檢查條件,并且把retry函數安排為原子操作。retry函數試圖對同一個互斥量進行加鎖,因此,除非互斥量是遞歸的,否則如果timeout函數直接調用retry就會導致死鎖。
2、讀寫鎖屬性
讀寫鎖與互斥量類似,也具有屬性。用pthread_rwlockattr_init初始化pthread_rwlockattr_t結構,用pthread_rwlockattr_destroy回收結構。
#include <pthread.h>int pthread_rwlockattr_init(pthread_rwlockattr_t *attr);int pthread_rwlockattr_destroy(pthread_rwlockattr_t *attr);兩者的返回值都是:若成功則返回0,否則返回錯誤編號
讀寫鎖支持的唯一屬性是進程共享屬性,該屬性與互斥量的進程共享屬性相同。就像互斥量的進程共享屬性一樣,用一對函數來讀取和設置讀寫鎖的進程共享屬性。
#include <pthread.h>int pthread_rwlockattr_getpshared(const pthread_rwlockattr_t * restrict attr, int *restrict pshared);int pthread_rwlockattr_setpshared(pthread_rwlockattr_t *attr, int pshared);兩者的返回值都是:若成功則返回0,否則返回錯誤編號
3、條件變量屬性
條件變量也有屬性。與互斥量和讀寫鎖類似,有一對函數用于初始化和回收條件變量屬性。
#include <pthread.h>int pthread_condattr_init(pthread_condattr_t *attr);int pthread_condattr_destroy(pthread_condattr_t *attr);兩者的返回值都是:若成功則返回0,否則返回錯誤編號
與其他的同步原語一樣,條件變量支持進程共享屬性。
#include <pthread.h>int pthread_condattr_getpshared(const pthread_condattr_t *restrict attr, int *restrict pshared);int pthread_condattr_setpshared(pthread_condattr_t *attr, int pshared);兩者的返回值都是:若成功則返回0,否則返回錯誤編號
4、屏障屬性
屏障也具有屬性。我們可以使用pthread_barrierattr_init函數初始化一個屏障屬性,使用pthread_barrierattr_destroy函數回收屏障屬性。
#include <pthread.h>int pthread_barrierattr_init(pthread_barrierattr_t *attr);int pthread_barrierattr_destroy(pthread_barrierattr_t attr);兩個函數的返回值都是:若成功則返回0,否則返回錯誤編號
唯一的一個屏障屬性是進程共享屬性。
#include <pthread.h>int pthread_barrierattr_getpshared(const pthread_barrierattr_t *restrict attr, int *restrict pshared);int pthread_barrierattr_setpshared(pthread_barrierattr_t *attr, int *pshared);兩個函數的返回值都是:若成功則返回0,否則返回錯誤編號
本篇博文內容摘自《UNIX環境高級編程》(第二版),僅作個人學習記錄所用。關于本書可參考:http://www.apuebook.com/。
新聞熱點
疑難解答