線程——《UNIX 環境高級編程》筆記

線程之表示

每個線程有一個ID,只在所属進程中有效。

pthread_t:表示線程ID。

pthread_equal比較兩個線程ID是否相等。

#include <pthread.h>
int pthread_equal(pthread_t tid1, pthread_t tid2);
// 返回值:若相等則返回非0,否則0

注意:pthread_t的類型並不一定是一個整型。在FreeBSD上就是用一個指向pthread結構的指針來表示的。

pthread_self函數可用來獲取線程自身的ID。

#include <pthread.h>
pthread_t thread_self(void);
// 返回值:調用線程的ID

線程之創建終止

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);
// 返回值:成功則返回0,否則返回錯誤編號
// 參數說明:
//   tidp:若成功,其所指內容被置為新線程ID
//   attr:用於設置線程的屬性。NULL表示使用默認屬性創建線程
//   start_rtn:當線程創建成功後,由此函數的地址開始運行。它帶有一個無類型指針參數,即arg
//   arg: 傳遞給start_rtn的參數。

注意:線程創建時不能保證是新線程還是調用線程先執行。新線程可能在pthread_create函數返回之前被執行。

如果進程中的任一線程調用了exit、_Exit或是_exit,則整個進程就會終止。與此類似,若信號的默認動作是終止進程,那麼該信號發送到的線程會終止整個進程。

單個線程可以通過下列三種於式退出而不終止整個進程:

  1. 線程從啟動例程中返回,返回值是線程的退出碼。

  2. 線程可以被同一進程中的其他線程取消。

  3. 線程調用pthread_exit。

#include <pthread.h>
void pthread_exit(void *rval_ptr);

rval_ptr是無類型指針,進程中的其他線程可通過pthread_join函數訪問這個指針。

如果主線程調用pthread_exit退出,它會阻塞到所有線程退出會才退出,可利用這點來等待所有線程完成。

#include <pthread.h>
int pthread_join(pthread_t thread, void **rval_ptr);
// 返回值:若成功則返回0,否則返回錯誤編號。

調用線程會被阻塞直到指定的線程退出。

如果線程是從啟動例程返回,則rval_ptr將包含返回碼;若是被取消的,由rval_ptr指定的內存單元就置為PTHREAD_CANCELED。

如果對線程的返回碼無興趣可將rval_ptr置為NULL。

通過調用pthread_join可自動使線程置於分離狀態,如此以來資源就可重新利用。

若一個線程已處於分離狀態,pthread_join就會失敗,返回EINVAL。


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

#include <pthread.h>
int pthread_cancel(pthread_t tid);
// 返回值:若成功則返回0,否則返回錯誤碼

默認情況下pthread_cancel函數會使線程表現為調用了參數為PTHREAD_CANCELED的pthread_exit函數。但線程可選擇忽略取消方式或是控制取消方式。

注意:pthread_cancel並不等待線程終止,它僅提出請求。

線程可以使用函數pthread_cleanup_push來安裝它退出時要執行的清理函數。

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

當線程執行以下動作時執行清理函數,調用參數為arg,調用的順序與pthread_cleanup_push函數安裝的順序相反:

  1. 調用pthread_exit時。

  2. 响應取消請求時。

  3. 用非零execute參數調用pthread_cleanup_pop時。

如果execute參數為零,清理函數將不被調用。無論哪種情況,pthread_cleanup_pop都會刪除上次pthread_cleanup_push調用建立的清理處理程序。

pthread_cleanup_push和pthread_cleanup_pop有一個限制:它們必須在與線程相同的作用域空間里成對的出現。因為它們可能實現為宏,而pthread_cleanup_push的宏定義可包含字符“{”,而pthread_cleanup_pop定義中包含與之匹配的“}”。(ps:在FreeBSD8.4上,如果線程pthread_cleanup_pop前返回,會出現段錯誤,因為這兩個函數實現為宏,而宏使用了局部變量,這個局部變量在調用清理函數時會用到,但是線程在pthread_cleanup_pop之前返回,這個局部變量就會失效,而此時清理函數還沒被系統調用,在調用的時候,這個局部變量已不可用了。)

默認情況下,線程的終止狀態會保存到對線程調用pthread_join,如果線程已經處於分離狀態,則其資源會在其結束時立即被收回。

pthread_join函數不能等待一個已分離的線程的終止狀態。

pthread_detach可使線程處於已分離狀態。

#include <pthread.h>
int pthread_detach(pthread_t tid);
// 返回值:成功則返回0,否則返回錯誤代碼

使用pthread_create函數的線程屬性參數也可創建一個處於分離狀能的線程。

線程之同步

當多個線程共享相同的內存時,需要确保每個線程看到一致的數據視圖。如果每個線程使用的變量都是其他線程不會讀取或修改的,或是變量是只讀的,就不會存在不一致問題。若當某個線程可修改變量,而其他線程可讀或是修改這個變量時,就需要對這些線程進行同步以确保它們在訪問變量的存儲內容時不會訪問到無效值。

1、互斥量

互斥量(mutex)從本質上說是一把鎖,在訪問共享資源前對互斥量進行加鎖,在訪問完成後釋放。對互斥量進行加鎖後,任何其他試圖再次對其加鎖的線程將會被阻塞直到當前線程釋放該互斥鎖。如果釋放互斥鎖時有多個線程阻塞,所有在該互斥鎖上阻塞的線程都會變成可運行狀態,第一個變為運行狀態的線程可以對互斥量加鎖,其他線程將會看到互斥鎖依然鎖著,只能回去再次等待它重新變為可用。在這种方式下,每次只有一個線程可以向前執行。

在設計時需要規定所有的線程必須遵守相同的數據訪問規則,只有這樣,互斥機制才能正常工作。操作系統並不會做數據訪問的串行化。

互斥變量用pthread_mutex_t數據類型表示,在使用前必須初始化。可把它置為常量PTHREAD_MUTEX_INITIALIZER(只對靜態分配的互斥量),也可通過調用pthread_mutex_init函數進行初始化。如果動態分配互斥量,那麼在釋放內存之前需要調用pthread_mutex_destroy。

#include <pthread.h>
int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);
int pthread_mutex_destroy(pthread_mutex_t *mutex);
// 返回值:成功返回0,否則返回錯誤代碼

要用默認的屬性初始化互斥量,只需將attr設置為NULL。

對互斥量加鎖要調用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);
// 返回值:若成功返回0,否則返回錯誤碼

如果線程不希望被阻塞可使用pthread_mutex_trylock嘗試對互斥量加鎖。如果調用pthread_mutex_trylock時互斥量未鎖住,那麼它將鎖住互斥量並返回0,否則就會失敗,返回EBUSY。

2、避免死锁

如果線程對同一個互斥量加鎖兩次,其自身會陷入死鎖。

若程序中使用多個互斥量,如果允許一個線程一直占有第一個互斥量,並試圖鎖住第二個互斥量時處於阻塞狀態,但拥有第二個互斥鎖的線程在試圖鎖住第一個互斥量,這時就會發生死鎖。因為兩個線程都在互相請求對方拥有的資源,但又不釋放自己拥有的資源,這樣兩個線程都無法進行下去,於是就産生了死鎖。

可以通過小心地控制互斥量加鎖的順序來避免死鎖。

有時候應用程序的結構使得對互斥量加鎖進行排序很困難,這種性況下可以用pthread_mutex_trylock嘗試加鎖,如果其成功返回,則繼續前進,否則的話,釋放掉自己的鎖,做好清理工作,過一段時間再嘗試加鎖。

多線程軟件設計,常要考慮鎖的設計。如果鎖的粒度太粗,就會出現很多線程等待相同的鎖,源自並發的改善就微乎其微;如果鎖的粒度太細的話,過多的鎖會使系統性能受到影响,而且代碼的設計會相當複雜。

3、讀寫鎖

讀寫鎖與互斥鎖類似,但允許更高的並行性。

讀寫鎖有三種狀態:讀模式下加鎖狀態,寫模式下加鎖狀態,不加鎖狀態。一次只有一個線程可以占有寫模式的讀寫鎖,但是多個線程可同時占有讀模式的讀寫鎖。

讀寫鎖實現雖各不相同,但當讀寫鎖處於讀模式鎖住狀態時,如果有另外的線程試圖以寫模式加鎖,讀寫鎖通常會阻塞隨後的讀模式鎖請求。這樣可以避免讀模式鎖長期占用而等待的寫模式鎖請求一直得不到满足。

讀寫鎖用數據類型pthread_rwlock_t表示。

讀寫鎖在使用前都要初始化,在釋放它們的底層內存前都必須鎖毀。

#include <pthread.h>
int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock,
                        const pthread_rwlockattr_t *restrict attr);
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
// 兩者的返回值都是:若成功返回0,否則返回鎖誤碼

pthread_rwlock_init對讀寫鎖進行初始化;attr為空時以默認屬性對其進行初始化。

釋放讀寫鎖之前用pthread_rwlock_destroy對其進行清理。

pthread_rwlock_rdlock以讀模式鎖定讀寫鎖

pthread_rwlock_wrlock以寫模式鎖定讀寫鎖

pthread_rwlock_unlock解除鎖定。

#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);
// 所有返回值都是:成功返回0,失敗返回鎖誤碼

在實現讀寫鎖的時侯,可能會對共享模式下可獲取的鎖的數量進行限制,所以需要檢查pthread_rwlock_rdlock的返回值。對於pthread_rwlock_wrlock和pthead_rwlock_unlock,如果鎖設計合理的話,可以不需要檢查其返回值。

讀寫鎖也有與互斥鎖類似的trylock。

#include <pthread.h>
int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);
// 若成功返回0,否則返回錯誤碼

4.條件變量

條件變量與互斥變量一起使用,允許線程以無竞爭的方式等待特定條件發生。

條件本身需要互斥量保護。線程在改變條件本身前必須先鎖住互斥量。

條件變量可以用常量PTHREAD_COND_INITIALIZER進行默認初始化,也可用pthread_cond_init函數進行初始化。使用pthread_cond_init初始化,則需要在釋於變量內存前使用pthread_cond_destroy對條件變量進行去初始化。

條件變量用pthread_cond_t類型表示。

#include <pthread.h>
int pthread_cond_init(pthread_cond_t *restrict cond, pthread_condattr_t *restrict attr);
int pthread_cond_destroy(pthread_cond_t *cond);
// 兩者返回值都是:成功返回0,否則返回錯誤編號

當attr為NULL時,創建一個默認的條件變量

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 timeout);
                          // 兩者的返回值都是:成功返回0,失敗返回出錯代碼


傳遞給pthread_cond_wait的互斥量對條件進行保護,調用者把鎖住的互斥量傳給函數。函數把調用線程放到等待條件的線程列表上,然後對互斥量解鎖,這兩個是原子操作。pthread_cond_wait在返回時互斥量再次被鎖住。

pthread_cond_timewait與pthread_cond_wait工作方式相似,只是多了一個timeout。timeout值指定了等待的時間。時間值用秒或者分秒數來表示,分秒數的單位是納秒。

struct timespec {
    time_t tv_sec;  /* 秒 */
    long   tv_nsec; /* 納秒 */
};

使用這個結構時需要指定等待多長時間,時間值為一個絶對數而不是相對數。例如,要等待3分鐘,就要在當前時間上加上3分鐘再轉換到timespec結構,而不是把3分鐘轉換成timespec結構。下面的函數將一個相對時間轉換為絶對時間:

void maketimeout(struct timespec *tsp, long minutes)
{
    struct timeval now;
    
    /* get the current time */
    gettimeofday(&now);
    tsp->tv_sec = now.tv_sec;
    tsp->tv_nsec = now.tv_usec * 1000; /* usec to nsec */
    /* add the offset to get timeout value */
    tsp->tv_sec += minutes * 60;
}

如果時間到了但是條件還是沒有出現,pthread_cond_timewait將重新獲取互斥量然後返回錯誤ETIMEOUT。注意:從pthread_cond_wait或者pthread_cond_timewait調用成功返回時需要重新計算條件,因為其他線程可能已經改變了條件。

pthread_cond_signal函數和pthread_cond_broadcast函數用來喚醒等待件的線程。pthread_cond_signal只喚醒某個線程,pthread_cond_broadcast喚醒所有等待該條件的線程。對於pthread_const_broadcast喚醒的所有線程,他們還要竞爭互斥量。

#include <pthread.h>
int pthread_cond_signal(pthread_cond_t *cond);
int pthread_cond_broadcast(pthread_cond_t *cond);
// 兩者都是成功返回0,失敗返回錯誤碼

條件變量的使用示例l書中程序清單11-9:

#include <pthread.h>
struct msg {
    struct msg *m_next;
    /* ... more stuff here ... */
};
struct msg *workq;
pthread_cond_t qready = PTHREAD_COND_INITIALIZER;
pthread_mutex_t qlock = PTHREAD_MUTEX_INITIALIZER;

void
pthread_msg(void)
{
    struct msg *mp;
    
    for (;;) {
        pthread_mutex_lock(&qlock);
        while (workq == NULL)
            pthread_cond_wait(&qready, &qlock);
        mp = workq;
        workq = mp->m_next;
        pthread_mutex_unlock(&qlock);
        /* now process the message mp */
    }
}

void
enqueue_msg(struct msg *mp)
{
    pthread_mutex_lock(&qlock);
    mp->m_next = workq;
    workq = mp;
    pthread_mutex_unlock(&qlock);
    pthread_cond_signal(&qready);
}


你可能感兴趣的:(UNIX編程,程序設計,UNIX環境高級編程,linux編程)