線程是程式中完成一個獨立任務的完整執行序列,即一個可調度的實體;進程相當於運行中程式的一種抽象。根據運行環境的調度者的身份,線程可分為內核線程和用戶線程。內核線程,在有的系統上稱為LWP(Light Weight Process,輕量級線程),運行在內核空間,由內核調度;用戶線程運行在用戶空間,由線 ...
線程是程式中完成一個獨立任務的完整執行序列,即一個可調度的實體;進程相當於運行中程式的一種抽象。根據運行環境的調度者的身份,線程可分為內核線程和用戶線程。內核線程,在有的系統上稱為LWP(Light Weight Process,輕量級線程),運行在內核空間,由內核調度;用戶線程運行在用戶空間,由線程庫來調度。當進程的一個內核線程獲得CPU的使用權時,它就載入並運行一個用戶線程。可見,內核線程相當於用戶線程運行的‘容器’,一個進程可以擁有M個內核線程和N個用戶線程,其中M<=N,並且一個系統的所有進程中,M和N的比值是固定的。
線程式控制制函數
pthread_create
#include <pthread.h> int pthread_create(pthread_t * tidp, const pthread_attr_t *attr, void *(*start_rtn)(void *), void *arg); // 返回:成功返回0,出錯返回錯誤編號
當pthread_create函數返回成功時,有tidp指向的記憶體被設置為新創建線程的線程ID,其類型pthread_t定義為:
#include <bits/pthreadtypes.h> typedef unsigned long int pthread_t;
attr參數用於設置各種不同的線程屬性,為NULL時表示預設線程屬性。新創建的線程從start_rtn函數的地址開始運行,該函數只有一個無類型指針的參數arg,如果需要向start_rtn函數傳入的參數不止一個,可以把參數放入到一個結構中,然後把這個結構的地址作為arg的參數傳入。
線程創建時並不能保證哪個線程會先運行:是新創建的線程還是調用線程。新創建的線程可以訪問調用進程的地址空間,並且繼承調用線程的浮點環境和信號屏蔽字,但是該線程的未決信號集被清除。那什麼是未決信號呢,信號產生到信號被處理這段時間間隔,稱信號是未決的。
pthread_exit
#include <pthread.h> void pthread_exit(void *rval_ptr); // 線程終止
線程在結束時最好調用該函數,以確保全全、乾凈的退出。pthread_exit函數通過rval_ptr參數向調用線程的回收者傳遞退出信息,進程中的其他線程可以調用pthread_join函數訪問到這個指針。pthread_exit執行完後不會返回到調用者,而且永遠不會失敗。
線程可以通過以下三種方式退出,在不終止整個進程的情況下停止它的控制流:
- 線程只是從啟動過程中退出,返回值是線程的退出碼
- 線程可以被同一進程中的其他線程取消
- 線程調用pthread_exit
pthread_join
#include <pthread.h> int pthread_join(pthread_t thread, void **rval_ptr); // 返回:成功返回0,出錯返回錯誤代碼
thread是目標線程標識符,rval_ptr指向目標線程返回時的退出信息,該函數會一直阻塞,直到被回收的線程結束為止。可能的錯誤碼為:
pthread_cancel
#include <pthread.h> int pthread_cancel(pthread_t thread); // 返回:成功返回0,出錯返回錯誤代碼
預設情況下,pthread_cancel函數會使有thread標識的線程的表現為如同調用了參數為PTHREAD_CANCEL的pthread_exit函數,但是,接收到取消請求的目標線程可以決定是否允許被取消以及如何取消,這分別由以下兩個函數來控制:
#include <pthread.h> int pthread_setcancelstate(int state, int *oldstate); int pthread_setcanceltype(int type, int *oldstate);
註意pthread_cancel並不等待線程結束,它只是提出請求。
互斥量
互斥量本質是一把鎖,在訪問公共資源前對互斥量設置(加鎖),確保同一時間只有一個線程訪問數據,在訪問完成後再釋放(解鎖)互斥量。在互斥量加鎖之後,其他線程試圖對該互斥量再次加鎖時都會被阻塞,知道當前線程釋放互斥鎖。如果釋放互斥量時有一個以上的互斥量,那麼所有在該互斥量上阻塞的線程都會變成可運行狀態,第一個變成運行的線程可以對互斥量加鎖,其他線程看到互斥量依然是鎖著的,只能再次阻塞等待該互斥量。
互斥量用pthread_mutex_t數據類型表示,在使用互斥量之前,必須使用pthread_mutex_init函數對它進行初始化,註意,使用完畢後需調用pthread_mutex_destroy。
#include <pthread.h> int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *mutexattr); int pthread_mutex_destroy(pthread_mutex_t *mutex); // 兩個函數返回值,成功返回0,否則返回錯誤碼
pthread_mutex_init用於初始化互斥鎖,mutexattr用於指定互斥鎖的屬性,若為NULL,則表示預設屬性。除了用這個函數初始化互斥所外,還可以用如下方式初始化:pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER。
pthread_mutex_destroy用於銷毀互斥鎖,以釋放占用的內核資源,銷毀一個已經加鎖的互斥鎖將導致不可預期的後果。
#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_lock以原子操作給一個互斥鎖加鎖。如果目標互斥鎖已經被加鎖,則pthread_mutex_lock則被阻塞,直到該互斥鎖占有者把它給解鎖。
pthread_mutex_trylock和pthread_mutex_lock類似,不過它始終立即返回,而不論被操作的互斥鎖是否加鎖,是pthread_mutex_lock的非阻塞版本。當目標互斥鎖未被加鎖時,pthread_mutex_trylock進行加鎖操作;否則將返回EBUSY錯誤碼。註意:這裡討論的pthread_mutex_lock和pthread_mutex_trylock是針對普通鎖而言的,對於其他類型的鎖,這兩個加鎖函數會有不同的行為。
pthread_mutex_unlock以原子操作方式給一個互斥鎖進行解鎖操作。如果此時有其他線程正在等待這個互斥鎖,則這些線程中的一個將獲得它。
互斥鎖使用示例:
/** * 使用3個線程分別列印 A B C */ #include <stdio.h> #include <stdlib.h> #include <string.h> #include <pthread.h> typedef struct ThreadInfo_t { char info; /* 'A' or 'B' or 'C' */ int n; /* remainder num */ int num; /* share num */ pthread_mutex_t mutex; }ThreadInfo; void *func(void *arg) { int cnt = 3; ThreadInfo *info = (ThreadInfo *)arg; int result = info->n; char show = info->info; while (cnt > 0) { if (info->num % 3 == result) { printf("---%c\n", show); pthread_mutex_lock(&info->mutex); info->num++; cnt--; pthread_mutex_unlock(&info->mutex); } } return NULL; } int main(int argc, char **argv) { pthread_t t1, t2, t3; ThreadInfo info; memset(&info, 0, sizeof(ThreadInfo)); pthread_mutex_init(&(info.mutex), NULL); info.n = 0; info.info = 'A'; pthread_create(&t1, NULL, func, &info); sleep(1); info.n = 1; info.info = 'B'; pthread_create(&t2, NULL, func, &info); sleep(1); info.n = 2; info.info = 'C'; pthread_create(&t3, NULL, func, &info); pthread_join(t1, NULL); pthread_join(t2, NULL); pthread_join(t3, NULL); return 0; }
讀寫鎖
讀寫鎖和互斥體類似,不過讀寫鎖有更高的並行性,互斥體要麼是鎖住狀態,要麼是不加鎖狀態,而且一次只有一個線程可以對其加鎖。而讀寫鎖可以有3個狀態,讀模式下鎖住狀態,寫模式下鎖住狀態,不加鎖狀態。一次只有一個線程可以占有寫模式的讀寫鎖,但是多個線程可以同時占用讀模式的讀寫鎖。讀寫鎖適合對數據結構讀的次數遠大於寫的情況。
當讀寫鎖是寫加鎖狀態時,在這個鎖被解鎖之前,所有試圖對這個鎖加鎖的線程都會被阻塞。當讀寫鎖是讀加鎖狀態時,所有試圖以讀模式對它進行加鎖的線程都可以得到訪問權,但是任何希望以寫模式對此鎖進行加鎖的線程都會阻塞,直到所有的線程釋放它們的讀鎖為止。
#include<pthread.h> int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock, const pthread_rwlockaddr_t *restrict attr); int pthread_rwlock_destroy(pthread_rwlock_t *restrict rwlock); // 成功返回0,否則返回錯誤碼
通過pthread_rwlock_init初始化讀寫鎖,如果希望讀寫鎖有預設屬性,可以傳一個NULL指針給attr。當不再需要讀寫鎖時,調用pthread_rwlock_destroy做清理工作。
#include<pthread.h> int pthread_rwlock_rdlock(pthread_rwlock_t *restrict rwlock); int pthread_rwlock_wrlock(pthread_rwlock_t *restrict rwlock); int pthread_rwlock_unlock(pthread_rwlock_t *restrict rwlock); // 成功返回0,否則返回錯誤碼
讀寫鎖的讀加鎖、寫加鎖和解鎖操作。
讀寫鎖程式示例:
/** * 兩個讀線程讀取數據,一個寫線程更新數據 */ #include <stdio.h> #include <stdlib.h> #include <string.h> #include <pthread.h> #define READ_THREAD 0 #define WRITE_THREAD 1 int g_data = 0; pthread_rwlock_t g_rwlock; void *func(void *pdata) { int data = (int)pdata; while (1) { if (READ_THREAD == data) { pthread_rwlock_rdlock(&g_rwlock); printf("-----%d------ %d\n", pthread_self(), g_data); sleep(1); pthread_rwlock_unlock(&g_rwlock); sleep(1); } else { pthread_rwlock_wrlock(&g_rwlock); g_data++; printf("add the g_data\n"); pthread_rwlock_unlock(&g_rwlock); sleep(1); } } return NULL; } int main(int argc, char **argv) { pthread_t t1, t2, t3; pthread_rwlock_init(&g_rwlock, NULL); pthread_create(&t1, NULL, func, (void *)READ_THREAD); pthread_create(&t2, NULL, func, (void *)READ_THREAD); pthread_create(&t3, NULL, func, (void *)WRITE_THREAD); pthread_join(t1, NULL); pthread_join(t2, NULL); pthread_join(t3, NULL); pthread_rwlock_destroy(&g_rwlock); return 0; }
條件變數
條件變數是線程可用的一種同步機制,條件變數給多個線程提供了一個回合的場所,條件變數和互斥量一起使用,允許線程以無競爭的方式等待特定的條件發生。條件變數本事是由互斥體保護的,線程在改變條件狀態之前必須首先鎖住互斥量,其他線程在獲取互斥量之前就不會覺察到這種變化,因為互斥量必須鎖定之後才改變條件。
#include<pthread.h> pthread_cond_init(pthread_cond_t *restrict cond, const pthread_condattr_t *restrict attr); pthread_cond_destroy(pthread_cont_t *cond); // 成功返回0,否則返回錯誤碼
使用條件變數前調用pthread_cond_init初始化,使用完畢後調用pthread_cond_destroy做清理工作。除非需要創建一個具有非預設屬性的條件變數,否則pthread_cond_init函數的attr參數可以設置為NULL。
#include<pthread.h> int pthread_cond_broadcast(pthread_cond_t *cond); int pthread_cond_signal(pthread_cond_t *cond); int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex); // 成功返回0,否則返回錯誤碼
傳遞給pthread_cond_wait的互斥量對條件進行保護,調用者把鎖住互斥量傳給函數,函數然後自動把調用線程放到等待條件的線程列表上,對互斥量解鎖。這就關閉了條件檢查和線程進入休眠狀態等待條件改變這兩個操作之間的時間通道,這樣線程就不會錯過條件的任何變化。pthread_cond_wait函數返回時,互斥量再次被鎖住。
pthread_cond_broadcast用廣播的形式喚醒所有等待條件變數的線程。pthread_cond_signal用於喚醒一個等待條件變數的線程,至於哪個線程被喚醒,取決於線程的優先順序和調度機制。有時候需要喚醒一個指定的線程,但pthread沒有對該需要提供解決方法。可以間接實現該需求:定義一個能夠唯一表示目標線程的全局變數,在喚醒等待條件變數的線程前先設置該變數為目標線程,然後以廣播形式喚醒所有等待條件變數的線程,這些線程被喚醒後都檢查改變數是否是自己,如果是就開始執行後續代碼,否則繼續等待。
條件變數程式示例:
#include <stdio.h> #include <stdlib.h> #include <pthread.h> #define err_sys(msg) \ do { perror(msg); exit(-1); } while(0) #define err_exit(msg) \ do { fprintf(stderr, msg); exit(-1); } while(0) pthread_cond_t cond; void *r1(void *arg) { pthread_mutex_t* mutex = (pthread_mutex_t *)arg; static int cnt = 10; while(cnt--) { printf("r1: I am wait.\n"); pthread_mutex_lock(mutex); pthread_cond_wait(&cond, mutex); /* mutex參數用來保護條件變數的互斥鎖,調用pthread_cond_wait前mutex必須加鎖 */ pthread_mutex_unlock(mutex); } return "r1 over"; } void *r2(void *arg) { pthread_mutex_t* mutex = (pthread_mutex_t *)arg; static int cnt = 10; while(cnt--) { //pthread_mutex_lock(mutex); //這個地方不用加鎖操作就行 printf("r2: I am send the cond signal.\n"); pthread_cond_signal(&cond); //pthread_mutex_unlock(mutex); sleep(1); } return "r2 over"; } int main(void) { pthread_mutex_t mutex; pthread_t t1, t2; char* p1 = NULL; char* p2 = NULL; pthread_mutex_init(&mutex, NULL); pthread_cond_init(&cond, NULL); pthread_create(&t1, NULL, r1, &mutex); pthread_create(&t2, NULL, r2, &mutex); pthread_join(t1, (void **)&p1); pthread_join(t2, (void **)&p2); pthread_cond_destroy(&cond); pthread_mutex_destroy(&mutex); printf("s1: %s\n", p1); printf("s2: %s\n", p2); return 0; }
自旋鎖
自旋鎖和互斥量類似,但它不是通過休眠使進程阻塞,而是在獲取鎖之前一直處於忙等(自旋)狀態,自旋鎖可用於下麵的情況:鎖被持有的時間短,並且線程不希望再重新調度上花費太多的成本。自旋鎖通常作為底層原語用於實現其他類型的鎖。根據他們所基於的系統架構,可以通過使用測試並設置指令有效地實現。當然這裡說的有效也還是會導致CPU資源的浪費:當線程自旋鎖變為可用時,CPU不能做其他任何事情,這也是自旋鎖只能夠被只有一小段時間的原因。
#include <pthread.h> int pthread_spin_init(pthread_spinlock_t *lock, int pshared); int pthread_spin_destroy(pthread_spinlock_t *lock);
pshared參數表示進程共用屬性,表明自旋鎖是如何獲取的,如果它設為PTHREAD_PROCESS_SHARED,則自旋鎖能被可以訪問鎖底層記憶體的線程所獲取,即使那些線程屬於不同的進程。否則pshared參數設為PTHREAD_PROCESS_PROVATE,自旋鎖就只能被初始化該鎖的進程內部的線程訪問到。
#include <pthread.h> int pthread_spin_lock(pthread_spinlock_t *lock); int pthread_spin_trylock(pthread_spinlock_t *lock); int pthread_spin_unlock(pthread_spinlock_t *lock);
如果自旋鎖當前在解鎖狀態,pthread_spin_lock函數不要自旋就可以對它加鎖,試圖對沒有加鎖的自旋鎖進行解鎖,結果是未定義的。需要註意,不要在持有自旋鎖情況下可能會進入休眠狀態的函數,如果調用了這些函數,會浪費CPU資源,其他線程需要獲取自旋鎖需要等待的時間更長了。
自旋鎖使用示例:
#include <stdio.h> #include <stdlib.h> #include <pthread.h> pthread_spinlock_t g_lock; int g_data = 0; void *func(void *arg) { while (1) { pthread_spin_lock(&g_lock); g_data++; printf("----------- %d\n", g_data); sleep(1); pthread_spin_unlock(&g_lock); } } int main(int argc, char **argv) { pthread_t tid; pthread_spin_init(&g_lock, PTHREAD_PROCESS_PRIVATE); pthread_create(&tid, NULL, func, NULL); pthread_create(&tid, NULL, func, NULL); pthread_create(&tid, NULL, func, NULL); pthread_join(tid, NULL); return 0; }
屏障
屏障是用戶協調多個線程並行工作的同步機制,屏障允許每個線程等待,直到所有合作的線程都到達某一點,然後從該點出繼續執行。pthread_join其實就是一種屏障,允許一個線程等待,直到另一個線程退出。但是屏障對象的概念更廣,它們允許任意數量的線程等待,直到所有的線程完成處理工作,而線程不需要退出,所有線程達到屏障後可以繼續工作。
#include <pthread.h> 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 *barrier); // 成功返回0,否則返回錯誤編號
初始化屏障時,可以使用count參數指定,在允許所有線程繼續運行前,必須達到屏障的線程數目。attr指定屏障屬性,NULL為預設屬性。
#include <pthread.h> int pthread_barrier_wait(pthread_barrier_t *barrier); // 成功返回0,否則返回錯誤編號
可以使用pthread_barrier_wait函數來表明,線程已完成工作,準備等所有其他線程趕過來。調用pthread_barrier_wait的線程在屏障計數未滿足條件時,會進入休眠狀態。如果該線程是最後一個調用pthread_barrier_wait的線程,則所有的線程會被喚醒。
一旦到達屏障計數值,而且線程處於非阻塞狀態,屏障就可以被重覆使用。
屏障使用示例:
#include <stdio.h> #include <stdlib.h> #include <pthread.h> pthread_barrier_t g_barrier; void *func(void *arg) { int id = (int )arg; if (id == 0) { printf("thread 0\n"); sleep(1); pthread_barrier_wait(&g_barrier); printf("thread 0 come...\n"); } else if (id == 1) { printf("thread 1\n"); sleep(2); pthread_barrier_wait(&g_barrier); printf("thread 1 come...\n"); } else if (id == 2) { printf("thread 2\n"); sleep(3); pthread_barrier_wait(&g_barrier); printf("thread 2 come...\n"); } return NULL; } int main(int argc, char **argv) { pthread_t t1, t2, t3; pthread_barrier_init(&g_barrier, NULL, 3); pthread_create(&t1, NULL, func, (void *)0); pthread_create(&t2, NULL, func, (void *)1); pthread_create(&t3, NULL, func, (void *)2); pthread_join(t1, NULL); pthread_join(t2, NULL); pthread_join(t3, NULL); return 0; }
參考:
1、《UNIX環境高級編程 第三版》線程章節