Linux多執行緒學習總結
原文:https://www.cnblogs.com/luoxn28/p/6087649.html
Linux多執行緒學習總結
執行緒是程式中完成一個獨立任務的完整執行序列,即一個可排程的實體;程序相當於執行中程式的一種抽象。根據執行環境的排程者的身份,執行緒可分為核心執行緒和使用者執行緒。核心執行緒,在有的系統上稱為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> pthread_mutex_t g_mutex; int g_cnt = 0; void *func(void *arg) { int loop = 3; int result = (int)arg; while (loop > 0) { if (g_cnt % 3 == result) { switch (result) { case 0: { printf("--- a\n"); break; } case 1: { printf("--- b\n"); break; } case 2: { printf("--- c\n"); break; } default: { return NULL; } } pthread_mutex_lock(&g_mutex); g_cnt++; loop--; pthread_mutex_unlock(&g_mutex); } } return NULL; } int main(int argc, char **argv) { pthread_t t1, t2, t3; pthread_mutex_init(&g_mutex, NULL); 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; }
讀寫鎖
讀寫鎖和互斥體類似,不過讀寫鎖有更高的並行性,互斥體要麼是鎖住狀態,要麼是不加鎖狀態,而且一次只有一個執行緒可以對其加鎖。而讀寫鎖可以有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環境高階程式設計 第三版》執行緒章節