線程因爲共享同一個進程的內存空間,因此資源的訪問也應當如同操做系統同樣受到限制,一個線程在讀取的時候其餘線程不能寫入,這種限制被稱爲同步措施。
在學習操做系統原理的時候應當都聽過鎖的使用。一個資源,若是想要被多個進程訪問,最好使用鎖機制來確保一致性,不會出現訪問衝突。線程也是同樣,對於這個狀況最簡單的想法就是簡單的加上一個鎖,同一時刻只容許一個線程訪問資源。
資源的訪問其實是競爭的訪問,若是說全部的操做都是原子性的,那麼線程就不存在資源衝突,可是很惋惜,目前作不到這點,因此咱們仍是不得不忍受着線程同步的繁瑣。
順便一提,目前現有的現代化操做系統,幾乎都是按照線程分配CPU的,而不是按照進程分配的,若是CPU大於線程數,甚至可讓一個線程佔據一個CPU資源,不過這種狀況不多見罷了。多線程
pthread模型提供了互斥的鎖訪問,也就是上面講過的同一時刻只有一個線程訪問資源。當設置了互斥量之後,任何視圖對互斥量加鎖的線程都會被阻塞直到當前線程釋放互斥鎖。
互斥變量是用pthread_mutex_t
數據類型聲明。而且在使用前,必須進行初始化,咱們也能夠將其設置爲常量PTHREAD_MUTEX_INITIALIZER
,也可使用pthread_mutex_init
函數初始化。若是是動態的分配變量,則在釋放內存前須要使用pthread_mutex_destroy
。函數
int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr); int pthread_mutex_destroy(pthread_mutex_t *mutex);
pthread_mutex_init函數建立一個新的互斥量,而且使用attr參數初始化,若是attr爲null,則使用默認的屬性參數。pthread_mutex_destroy釋放分配給mutex參數的資源。
下面是加鎖函數性能
int pthread_mutex_lock(pthread_mutex_t *mutex); int pthread_mutex_trylock(pthread_mutex_t *mutex); int pthread_mutex_unlock(pthread_mutex_t *mutex);
這三個函數都很簡單,lock函數會阻塞調用進程,trylock函數則嘗試鎖住資源,若是失敗則不會阻塞。
下面講述原著中一個例子,關於C語言引用計數的線程同步。學習
#include <stdlib.h> #include <pthread.h> struct foo { int f_count; pthread_mutex_t f_lock; int f_id; /* more stuff here */ } struct foo *foo_alloc(int id); { struct foo *fp; if ((fp = malloc(sizeof(struct foo))) != NULL) { fp->f_count = 1; fp->f_id = id; if (pthread_mutex_init(&fp->f_lock, NULL) != 0) { free(fp); return(NULL); } /* continue initialization */ } return(fp); } void foo_hold(struct foo *fp) { pthread_mutex_lock(&fp->f_lock); ++fp->f_count; pthread_mutex_unlock(&fp->f_lock); } void foo_rele(struct foo*fp) { pthread_mutex_lock(&fp->f_lock); if (--fp->f_count == 0) { pthread_mutex_lock(&fp->f_lock); pthread_mutex_destroy(&fp->lock); free(fp); } else { pthread_mutex_unlock(&fp->f_lock); } }
引用計數是一種古老的內存管理計數,很簡單,可是很是有效,性能也很高,上面就是一種C語言下的引用計數,固然,咱們能夠將函數以函數指針的形式放置在結構體中,這裏就不弄了。
能夠看到,結構體很是簡單,就一個引用計數成員、互斥量和數據成員,使用foo_alloc函數分配空間,而且在其中初始化互斥量,在foo_alloc函數中咱們並無使用互斥量,由於初始化完畢前分配線程是惟一的能使用的線程。可是在這裏例子中,若是一個線程調用foo_rele釋放引用,可是在此期間另外一個線程使用foo_hold阻塞了,等第一個線程調用完畢,引用變爲0,而且內存被回收,而後就會致使崩潰。操作系統
死鎖也是個很常見的狀況,一個線程試圖對同一個互斥量加鎖兩次,那麼它自身會陷入死鎖狀態,再好比兩個線程都在等待對方已經佔有的資源,也會致使死鎖,死鎖一直是新手的大忌,因此應當仔細控制加鎖來避免。pthread_mutex_trylock就是一個很好的方法用來防止死鎖產生。線程
#include <stdlib.h> #include <pthread.h> #define NHASH 29 #define HASH(id) (((unsigned long)id)%NHASH) struct foo *fh[NHASH]; pthread_mutex_t hashlock = PTHREAD_MUTEX_INITIALIZER; struct foo { int f_count; pthread_mutex_t f_lock; int f_id; struct foo *f_next; /* more stuff here */ } struct foo *foo_alloc(int id); { struct foo *fp; int idx; if ((fp = malloc(sizeof(struct foo))) != NULL) { fp->f_count = 1; fp->f_id = id; if (pthread_mutex_init(&fp->f_lock, NULL) != 0) { free(fp); return(NULL); } idx = HASH(id); pthread_mutex_lock(&hashlock); fp->f_next = fh[idx]; fh[idx] = fp; pthread_mutex_lock(&fp->f_lock); pthread_mutex_unlock(&hashlock); /* continue initialization */ pthread_mutex_unlock(&fp->f_lock); } return(fp); } void foo_hold(struct foo *fp) { pthread_mutex_lock(&fp->f_lock); ++fp->f_count; pthread_mutex_unlock(&fp->f_lock); } struct foo *foo_find(int id) { struct foo *fp; pthread_mutex_lock(&hashlock); for (fp = fh[HASH(id)]; fp != NULL; fp = fp->f_next) { if (fp->f_id == id) { foo_hold(fp); break; } } pthread_mutex_unlock(&hashlock); return(fp); } void foo_rele(struct foo*fp) { struct foo *tfp; int idx; pthread_mutex_lock(&fp->f_lock); if (fp->f_count == 1) { pthread_mutex_unlock(&fp->f_lock); pthread_mutex_lock(&hashlock); pthread_mutex_lock(&fp->f_lock); if (fp->f_count != 1) { --fp->f_cound; pthread_mutex_unlock(&fp->f_lock); pthread_mutex_unlock(&hashlock); return; } idx = HASH(fp->f_id); tfp = fh[idx]; if (tfp == fp) { fh[idx] = fp->f_next; } else { while (tfp->f_next != fp) tfp = tfp->f_next; tfp->f_next = fp->f_next; } pthread_mutex_unlock(&hashlock); pthread_mutex_unlock(&fp->f_lock); pthread_mutex_destroy(&fp->lock); free(fp); } else { --fp->f_count; pthread_mutex_unlock(&fp->f_lock); } }
這是對以前一個例程的改進,程序很容易理解,這裏維護了兩個互斥量,一個全局互斥量,一個結構體內部的互斥量,或者換言之,全局互斥量是哈希表自身的互斥量,每次的時候,先加鎖全局,而後再加鎖結構體內部,就能避免死鎖。其實上面添加的那麼多東西,實際上就是哈希散列公式,而後利用哈希散列公式存取內部的結構體變量。
看了那麼久了,可能你們也對其中實現細節很好奇,這裏就稍微貼一下代碼指針
struct _opaque_pthread_mutex_t { long __sig; char __opaque[__PTHREAD_MUTEX_SIZE__]; }; typedef struct _opaque_pthread_mutex_t __darwin_pthread_mutex_t; typedef __darwin_pthread_mutex_t pthread_mutex_t; #define PTHREAD_MUTEX_INITIALIZER {_PTHREAD_MUTEX_SIG_init, {0}}
相信看了上面的代碼,各位對互斥量實現細節應該也有本身的見解了。rest
int pthread_mutex_timedlock(pthread_mutex_t *restrict mutex, const struct timespec *restrict tsptr);
這個函數其實沒什麼好說的,只是在原先的鎖定阻塞函數上多了一個超時機制,可是感受沒什麼用,在實際使用的時候也不多出現,並且很悲傷的是,蘋果系統還沒有支持這個函數,雖然FreeBSD函數支持。code
讀寫鎖是一個更加靈活的鎖機制,互斥鎖只有兩種狀態,鎖定、不鎖定,而讀寫鎖能夠有三種狀態,讀鎖定、寫鎖定、不加鎖狀態。而且在讀鎖定狀況下,能夠有不少線程鎖定,而寫鎖定下,只有一個線程能鎖定。其實很是好理解,讀取能夠有多個,可是寫入只能有一個,當寫入的時候就不能讀取,讀取的時候不能寫入。在不少教科書上,讀寫鎖也被稱爲共享互斥鎖。接口
int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock, const pthread_rwlockattr_t *restrict attr); int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
這兩個函數跟以前互斥鎖的初始化函數差很少,系統也提供了PTHREAD_RWLOCK_INITIALIZER。就像前面提到的pthread各類類型的實現同樣,因爲這些變量本質上是一個結構體,也是會分配內存空間的,若是咱們在調用上面函數銷燬回收空間以前就使用free函數回收內存,則會致使資源丟失,也就是內存泄露。
int pthread_rwlock_rdlock(pthread_rwlock_t *lock); int pthread_rwlock_wrlock(pthread_rwlock_t *lock); int pthread_rwlock_unlock(pthread_rwlock_t *lock);
這三個函數實際上也就是三種讀寫鎖的狀態,並且這些函數實際上也有條件版本。
int pthread_rwlock_tryrdlock(pthread_rwlock_t *lock); int pthread_rwlock_trywrlock(pthread_rwlock_t *lock);
就像互斥鎖同樣,Unix標準還規定了帶有超時的讀寫鎖,可是很是遺憾,蘋果系統一樣不存在這個函數。因此這裏就不講解了
int pthread_rwlock_timedrdlock(pthread_rwlock_t *restrict rwlock, const struct *timespec *restrict tsptr); int pthread_rwlock_timedrdlock(pthread_rwlock_t *restrict rwlock, const struct *timespec *restrict tsptr);
條件變量是第三種同步機制。條件變量讓線程掛起,直到共享數據上的某些條件獲得知足才觸發啓動。可是在正常狀況下須要和互斥量一塊兒使用,來防止出現條件競爭。
就像是其餘的pthread函數同樣,咱們必須先對其進行初始化後才能使用,一樣也存在PTHREAD_COND_INITIALIZER常量對其進行初始化。
int pthread_cond_init(pthread_cond_t *restrict cond, const pthread_condattr_t *restrict attr); int pthread_cond_destroy(pthread_cond_t *cond);
建立完成之後,須要使用函數等待條件變爲真
int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex); int pthread_cond_timedwait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex, const struct timespec *restrict abstime);
使人驚訝的是,蘋果系統竟然提供了超時函數。固然,就像是鎖定函數同樣,這兩個函數都是阻塞的。pthread_cond_wait
函數自動解鎖mutex參數指向的互斥量,並使當前線程阻塞在cond參數指向的條件變量上,被阻塞的線程能夠被pthread_cond_signal
函數函數或者pthread_cond_broadcast
喚醒,當函數返回的時候,互斥量將被再次鎖住。
int pthread_cond_signal(pthread_cond_t *cond); int pthread_cond_broadcast(pthread_cond_t *cond);
這兩個函數很好理解,就是發送了一個信號給cond參數指向的條件。
自旋鎖是爲實現保護共享資源而提出一種鎖機制。其實,自旋鎖與互斥鎖比較相似,它們都是爲了解決對某項資源的互斥使用。不管是互斥鎖,仍是自旋鎖,在任什麼時候刻,最多隻能有一個保持者,也就說,在任什麼時候刻最多隻能有一個執行單元得到鎖。可是二者在調度機制上略有不一樣。對於互斥鎖,若是資源已經被佔用,資源申請者只能進入睡眠狀態。可是自旋鎖不會引發調用者睡眠,若是自旋鎖已經被別的執行單元保持,調用者就一直循環在那裏看是否該自旋鎖的保持者已經釋放了鎖,"自旋"一詞就是所以而得名。實際上自旋鎖和互斥鎖在接口形式和行爲方面都很是類似,能夠很容易的從一個到另一個,不過,蘋果系統並無提供自旋鎖,因此這裏也就不講述了。
屏障是用戶協調多個線程並行工做,而且直到全部線程都到達一點之後繼續執行,pthread_join函數就是一種屏障。Unix系統還提供了同遊的函數用於開發。
int pthread_barrier_init(pthread_barrier_t *restrict barrier, const pthread_barrierattr_t *restrict attr, unsigned int count); int pthread_barrier_destroy(pthread_barrier_t *restrict barrier); int pthread_barrier_wait(pthread_barrier_t *barrier)
其實這些API通過以前的學習,基本上都能看懂了,就是和互斥鎖同樣,只不過更加的普遍而已。可是很是遺憾的是,蘋果系統仍是沒有提供這些API,或者是蘋果認爲這些API並無想象中那麼好用,而且在實際使用中也是更少見,因此就將其depreciation。