線程是CPU調度的最小單位,也就是說一個進程中的多個線程是以不定的調度順序並發執行的。而一個進程中的多個線程是共用記憶體資源的,這裡就引出了一個概念----臨界資源,多個線程都可以訪問的資源,而線程中訪問臨界資源的程式碼片段稱為臨界區。如果多個線程都有同一個臨界資源的臨界區,那這些線程的調用順序不同得到的結果就可能不同,這時我們就需要一些措施使線程按照我們的想法順序執行從而避免這種情況情況,這個過程稱之為線程的同步。
線程同步的第一種措施是互斥量。互斥量實際上是一把鎖,相關的操作主要是在臨界區前加鎖、臨界區後解鎖;互斥量的類型是pthread_mutex_t。
該類型的初始化和銷毀有靜態方法和動態方法,其中靜態方法很簡單:pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;動態方法是使用pthread_mutex_init()和pthread_mutex_destroy()函數。
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr);int pthread_mutex_destory(pthread_mutex_t *mutex);
這兩種方法在使用時的區別主要在於,第二種方法可以指定互斥量的屬性,如果預設屬性傳NULL即可。
加鎖、解鎖操作如下:
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_lock()函數是加鎖操作,如果mutex鎖處於未加鎖狀態,則加鎖成功繼續執行,否則該函數會阻塞至可以加鎖為止。pthread_mutex_unlock()函數是解鎖操作,如果鎖的狀態是已加鎖,則解鎖否則返回錯誤。pthread_mutex_trylock()函數的功能是嘗試加鎖,如果鎖處於已加鎖狀態時不阻塞而是返回EBUSY,一般用於可以加鎖時訪問臨界區否則執行另一個操作的情況下。
這裡的臨界資源,並不僅限於全域變數這樣每個線程都能訪問到的資源,還包括進程中使用的一些其他資源如檔案。在多個線程中訪問同一個檔案的代碼也是臨界區,所以也需要使用互斥量。Linux系統還給我們提供了現成的檔案不同操作函數(原理就是使用了互斥量)。
void flockfile(FILE *filehandle);int ftrylockfile(FILE *filehandle);void funlockfile(FILE *filehandle);
這三個函數的功能及使用方法是和互斥量的加鎖解鎖操作一樣的。
在多個線程可以調用同一個函數,使用pthread_once()函數可以指定讓該函數在所有線程只被調用一次(被哪個調用不一定)
int pthread_once(pthread_once_t *once_control, void (*init_routine)(void));pthread_once_t once_control = PTHREAD_ONCE_INIT;
pthread_once()的功能是調用init_routine函數,並保證在整個進程中只被調用一次。
還有一種和處理量類似的鎖----讀寫鎖,讀寫鎖和互斥鎖的區別是:對於臨界區加的鎖有兩種,共用式讀鎖和獨佔式寫鎖,如果臨界區中對臨界資源只做讀的操作則使用讀鎖即可,否則需要寫鎖。讀鎖可以加上多把,但寫鎖只能加一把(並且不能有讀鎖)。相關操作如下:
pthread_rwlock_t rwlock = PTHREAD_RWLOCK_INITIALIZE;int pthread_rwlock_init(pthread_rwlock_t * rwlock, const pthread_rwlockattr_t * attr);int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);int pthread_rwlock_rwlock(pthread_rwlock_t *rwlock);int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);
這裡所有的操作和互斥量都是類似的。在使用上需要注意點的是,同一時刻只能有一把寫鎖並且不能有讀鎖或者可以由不定數量的讀鎖。
互斥量經常和條件變數一起配合使用,條件變數並不是鎖,而是一種等待--通知機制。等待的是條件,通知的是條件達成,提供了線程間的等待條件的方式。條件變數的類型pthread_cond_t,相關的初始化銷毀操作如下:
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;int pthread_cond_init(pthread_cond_t * cond, const pthread_condattr_t *attr); int pthread_cond_destroy(pthread_cond_t *cond);
條件變數上的操作是等待和通知,用於等待條件的函數:
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 * timeout);
pthread_cond_wait()函數的功能分三步:1.將mutex解鎖 2.等待通知條件達成 3.將mutex解鎖;在等待通知時該線程是處於阻塞狀態的。pthread_cond_timedwait()函數與pthread_cond_wait()函數相比,多了一個時間結構,是用於逾時計時的,在第二步阻塞時如果超過了這個時間,則會立即返回且返回的錯誤編碼是ETIMEDOUT。注意這個時間並不是時間段而是一個時間點。
通知條件達成的函數如下:
int pthread_cond_broadcast(pthread_cond_t *cond); int pthread_cond_signal(pthread_cond_t *cond);
這個函數都是用於通知其餘線程條件達成,解除第二步的阻塞,這樣被通知的線程就可以再次嘗試加鎖並繼續執行。這兩個函數的區別是:pthread_cond_signal()用於通知所有處於wait狀態的線程中的某一個解除阻塞並嘗試加鎖,而pthread_cond_broadcast()用於通知所有處於wait狀態的線程都解除阻塞並嘗試加鎖。條件何時達成通知何時發送是由編程員自己確定的,並一定要在條件達成後再發送通知;如果條件在wait前已經達成,則有可能使wait永遠等待下去(發送的通知不會一直留著,如果沒有接受的也會自動消失)。
最後一種同步的措施是使用無名訊號量。訊號量也稱為號誌,燈亮時表示資源可以使用(可以進入臨界區),燈滅時表示無資源可用(不可以進入臨界區 阻塞);並且訊號量是一種多燈,也就是說燈的數量可以是多個,但只要有一個亮可以進入臨界區;每次進入臨界區後應滅一盞燈,每次出臨界區後應點亮一盞燈。相關的操作如下:
int sem_init(sem_t *sem, int pshared, unsigned int value); int sem_destroy(sem_t *sem);int sem_wait(sem_t *sem); int sem_post(sem_t *sem); int sem_getvalue(sem_t *sem, int *sval);
sem_init()函數用來初始化一個訊號量,pshared為0用於線程間,非0用於進程間;value參數是初始化燈的個數。sem_destroy()函數用來銷毀一個訊號量。sem_wait()函數用於滅燈,如果無燈可滅則阻塞至有燈亮時再次嘗試滅燈;sem_post()函數用於點燈;setm_getvalue()用來獲得被點亮的燈的個數。
說的鎖,不得不提的是死結的概念。死結:兩個或兩個以上的線程(進程)在執行過程中,因競爭資源而造成的一種互相等待的現象。死結產生的原因:資源競爭以及線程(進程)推進順序非法。死結產生的條件:互斥條件、請求和保持條件、不可剝奪條件、環路等待條件。死結處理方法:預防死結(破壞條件中的任意一個)、避免死結(縮短事務的邏輯處理過程、死結逾時、最佳化程式、仔細測試、錯誤處理)