我們已經了解進程的基本概念:進程是正在執行的程序,并且是系統資源分配的基本單位。當用戶需要在一臺計算機上去完成多個獨立的工作任務時,可以使用多進程的方式,為每個獨立的工作任務分配一個進程。多進程的管理則由操作系統負責——操作系統調度進程,合理地在多個進程之間分配資源,包括CPU資源、內存、文件等等。除此以外,即便是一個單獨的應用,采用多進程的設計也可以提升其吞吐量,改善其響應時間。假如在處理任務的過程中,其中一個進程因為死循環或者等待IO之類的原因無法完成任務時,操作系統可以調度另一個進程來完成任務或者響應用戶的請求。比如在文本處理程序當中,可以并發地處理用戶輸入和保存已完成文件的任務。
隨著進程進入大規模使用,程序員在分析性能的時候發現計算機花費了大量的時間在切換不同的進程上面。當一個進程在執行過程中,CPU的寄存器當中需要保存一些必要的信息,比如堆棧、代碼段等,這些狀態稱作上下文。上下文切換這里涉及到大量的寄存器和內存之間的保存和載入工作。除此以外,Linux操作系統還支持虛擬內存,所以每個用戶進程都擁有自己獨立的地址空間,這在實現上就要求每個進程有自己獨立的頁目錄表。所以,具體到Linux操作系統,進程切換包括兩步:首先是切換頁目錄表,以支持一個新的地址空間;然后是進入內核態,將硬件上下文,即CPU寄存器的內容以及內核態棧切換到新的進程。
為了縮小創建和切換進程的開銷,線程的概念便誕生了。線程又被稱為輕量級進程(Light Weight
Process, LWP),我們將一個進程分解成多個線程,每個線程是獨立的運行中程序實體,線程之間并發運行,這樣線程就取代進程成為CPU時間片的分配和調度的最小單位,在Linux操作系統當中,每個線程都擁有自己獨立的 task_struct 結構體,當然,在屬于同一個進程多個線程中, task_struct 中大量的字段是相同的或者是共享的。
注意到在Linux操作系統中,線程并沒有脫離進程而存在。而計算機的內存、文件、IO設備等資源依然還是按進程為單位進行分配,屬于同一個進程的多個線程會共享進程地址空間,每個線程在執行過程會在地址空間中有自己獨立的棧,而堆、數據段、代碼段、文件描述符和信號屏蔽字等資源則是共享的。
同一個進程的多個線程各自擁有自己的棧,這些棧雖然是獨立的,但是都位于同一個地址空間中,
所以一個線程可以通過地址去訪問另一個線程的棧區。
因為屬于同一個進程的多個線程是共享地址空間的,所以線程切換的時候不需要切換頁目錄表,而且上下文的內容中需要切換的部分也很少,只需要切換棧和PC指針以及其他少量控制信息即可,數據段、代碼段等信息可以保持不變。因此,切換線程的花費要小于切換進程的。
在Linux文件系統中,路徑 /proc 掛載了一個偽文件系統,通過這個偽文件系統用戶可以采用訪問
普通文件的方式(使用read系統調用等),來訪問操作系統內核的數據結構。其中,在 /proc/[num] 目錄中,就包含了pid為num的進程的大量的內核數據。而在 /proc/[num]/task就包含本進程中所有線程的內核數據。我們之前的編寫進程都可以看成是單線程進程。
用戶級線程和內核級線程
根據內核對線程的感知情況,線程可以分為用戶級線程和內核級線程。操作系統內核無法調度用戶級線程,所以通常會存在一個管理線程(稱為運行時),管理線程負責在一個用戶線程陷入阻塞的切換到另一個線程。如果線程無法陷入阻塞,則用戶需要在代碼當中主動調用yield以主動讓出,否則一個運行中的線程將永遠地占用CPU。用戶級線程在CPU密集型的任務中毫無作用,而且也無法充分多核架構的優勢,所以幾乎所有的操作系統都支持內核級線程。
由于用戶級線程在處理IO密集型任務的時候有著一定的優勢,所以目前在一些服務端框架中,混合性線程也引入使用了——在這種情況下,會把用戶級線程稱為有棧協程。應用程序會根據硬件中CPU的核心數創建若干個內核級線程,每一個內核級線程會對應多個有棧協程。這樣在觸發IO操作時,不再需要陷入內核態,直接在用戶態切換線程即可。
目前使用最廣泛的線程庫名為NPTL (Native POSIX Threads Library),在Linux內核2.6版本之后,它取代了傳統的LinuxThreads線程庫。NPTL支持了POSIX的線程標準庫,在早期,NPTL只支持用戶級線程,隨著內核版本的迭代,現在每一個用戶級線程都對應一個內核態線程,這樣NPTL線程本質就成為了內核級線程,可被操作系統調度。
線程的創建和終止
使用線程的思路其實和使用進程的思路類似,用戶需要去關心線程的創建、退出和資源回收等行為,初學者可以參考之前學習進程的方式對比學習線程對應的庫函數。下面是常用的線程庫函數和之前的進程的對應關系。
線程 | 函數功能 | 類似的進程函數 |
pthread_create | 創建一個線程 | fork |
pthread_exit | 線程退出 | exit |
pthread_join | 等待線程結束并回收資源 | wait |
pthread_self | 獲取線程id | getpid |
在使用線程相關的函數之后,在鏈接時需要加上 -lpthread 選項以顯式鏈接線程庫。
線程函數的錯誤處理
之前的POSIX系統調用和庫函數在調用出錯的時候,通常會把全局變量 errno 設置為一個特別的數值以指示報錯的類型,這樣就可以調用 perror 以顯示符合人閱讀需求的報錯信息。但是在多線程編程之中,全局變量是各個線程的共享資源,很容易被并發地讀寫,所以pthread系列的函數不會通過修改 errno來指示報錯的類型,它會根據不同的錯誤類型返回不同的返回值,使用 strerror 函數可以根據返回值顯示報錯字符串。
char *strerror(int errnum);
define THREAD_ERROR_CHECK(ret,msg) {if(ret!=0){fprintf(stderr,"%s:%s\n",msg,strerror(ret));}}
創建線程
pthread_create
線程創建使用的函數是 pthread_create ,這個函數的函數原型如下:
#include <pthread.h>int pthread_create(pthread_t *thread, const pthread_attr_t *attr,void *(*start_routine)(void *), void *arg);
參數說明
pthread_t *thread
用于存儲新創建線程的線程 ID。線程 ID 是線程的唯一標識符,類型為pthread_t
。在不同的操作系統中,pthread_t底層實現不同,具體到Linux是一個無符號整型數,使用函數 pthread_self 可以獲取本線程的id。const pthread_attr_t *attr
指定線程的屬性。如果不需要設置特殊屬性,可以傳入NULL
,使用默認屬性。void *(*start_routine)(void *)
指定線程的入口函數。該函數必須接受一個void *
類型的參數,并返回一個void *
類型的值。void *arg
傳遞給線程入口函數的參數。如果不需要傳遞參數,可以傳入NULL
。
返回值
成功時返回
0
。失敗時返回錯誤碼(非零值),例如
EAGAIN
(資源不足)、EINVAL
(無效參數)等。
?示例:
void * threadFunc(void *arg){printf("I am child thread, tid = %lu\n", pthread_self());return NULL;
}
int main(){pthread_t tid;int ret = pthread_create(&tid,NULL,threadFunc,NULL);THREAD_ERROR_CHECK(ret,"pthread_create");printf("I am main thread, tid = %lu\n", pthread_self());sleep(1);return 0;
}
如果沒有設置sleep(1)導致主線程提前退出,我們可能會看到這樣一個現象,標準輸出上有一定的幾率顯示兩條相同的語句。產生這種問題的原因是 stdout 緩沖區在多個線程之間是共享,當執行 printf 時,會首先將stdout的內容拷貝到內核態的文件對象中,再清空緩沖區,當主線程終止導致所有線程終止時,可能子線程已經將數據拷貝到了內核態(此時第一份語句已經打印了),但是stdout的內容并未清空,此時進程終止,會把所有緩沖區清空,清空緩沖區的行為會將留存在緩沖區的內容直接清空并輸出到標準輸出中,此時就會出現內容的重復打印了。
線程和數據共享
共享全局變量
int global = 2;
void *threadFunc(){printf("I am child thread, tid = %lu\n", pthread_self());printf("%d\n", global);
}int main(int argc, char const *argv[]){pthread_t tid;int ret = pthread_create(&tid, NULL, threadFunc, NULL);THREAD_ERROR_CHECK(ret,"pthread_create");printf("I am main thread, tid = %lu\n", pthread_self());sleep(1);return 0;
}
共享堆
void *threadFunc(void *arg){printf("I am child thread, tid = %lu\n", pthread_self());char *p = (void *)arg;printf("%c\n", *p);
}int main(int argc, char const *argv[])
{char *p = (char*)malloc(sizeof(char));*p = 'C';pthread_t tid;int ret = pthread_create(&tid, NULL, threadFunc, (void *)p);THREAD_ERROR_CHECK(ret,"pthread_create");printf("I am main thread, tid = %lu\n", pthread_self());sleep(1);return 0;
}
雖然說 arg 是一個 void* 類型的參數,這暗示著用戶可以使用該參數來傳遞一個數據的地址,但是有些情況下我們只需要傳遞一個整型數據,在這種情況,用戶可以直接把 void* 類型的參數當成是一個8字節的普通數據(比如long)進行傳遞。
訪問另一個線程的棧
雖然各個線程執行的過程中擁有自己獨立的棧區,但是這些所有的棧區都是在同一個地址空間當中,所以一個線程完全可以訪問到另一個線程棧幀內部的數據。
void *threadFunc(void *arg){printf("I am child thread, tid = %lu\n", pthread_self());int *p = (int *)arg;printf("%d\n", *p);
}int main(int argc, char const *argv[])
{pthread_t tid;int num = 101;int ret = pthread_create(&tid, NULL, threadFunc, (void*)&num);THREAD_ERROR_CHECK(ret,"pthread_create");printf("I am main thread, tid = %lu\n", pthread_self());sleep(1);return 0;
}
如果將主線程棧幀數據的地址作為參數傳遞給各個子線程,就一定要注意并發訪問的情況,有可能另一個線程的執行會修改掉原本想要傳遞數據的內容。
線程主動退出
使用 pthread_exit 函數可以主動退出線程,無論這個函數是否是在 start_routine 中被調用,其行為類似于進程退出的 exit 。 pthread_exit 的參數是一個 void * 類型值,它描述了線程的退出狀態。在start_routine 中使用return語句可以實現類似的主動退出效果,但是其被動退出的行為有一些問題,所以使用較少。線程的退出狀態可以由另一個線程使用 pthread_join 捕獲,但是和進程不一樣的是,另一個線程的選擇是任意的,不需要是線程創建者。
pthread_exit
#include <pthread.h>void pthread_exit(void *retval);
參數
void *retval
該參數是線程的返回值,類型為void *
。其他線程可以通過pthread_join
獲取這個返回值。如果線程不需要返回值,可以傳入NULL
。
功能
當線程調用
pthread_exit
時,當前線程會立即終止執行。如果線程是被其他線程通過
pthread_join
等待的,pthread_exit
的返回值會傳遞給pthread_join
。如果線程是分離的(detached),線程資源會在線程終止時自動釋放。
返回值
pthread_exit
是一個有去無回的函數,它不會返回。一旦調用,當前線程就會終止。
示例:
void * threadFunc(void *arg){printf("I am child thread, tid = %lu\n", pthread_self());pthread_exit(NULL); //和在線程入口函數return(NULL)等價printf("Can you see me?\n");
}
獲取線程退出狀態
調用 pthread_join 可以使本線程處于等待狀態,直到指定的 thread 終止,就結束等待,并且捕獲到的線程終止狀態存入 retval 指針所指向的內存空間中。因為線程的終止狀態是一個 void * 類型的數據,所以 pthread_join 的調用者往往需要先申請8個字節大小的內存空間,然后將其首地址傳入,在pthread_join 的執行之后,這里面的數據會被修改。有些時候,內容可能是一片數據的首地址,還有些情況下內容就是簡單的一個8字節的整型。
pthread_join
#include <pthread.h>int pthread_join(pthread_t thread, void **retval);
參數說明
pthread_t thread
指定要等待的線程的 ID。這個 ID 是通過pthread_create
創建線程時返回的。void **retval
用于存儲被等待線程的返回值。如果不需要獲取返回值,可以傳入NULL
。如果需要獲取返回值,必須傳入一個void *
類型的指針的地址。
返回值
成功時返回
0
。失敗時返回錯誤碼(非零值),例如:
EINVAL
:指定的線程 ID 無效。ESRCH
:指定的線程不存在。EINVAL
:指定的線程已經被分離(detached)。
功能
pthread_join
會阻塞調用它的線程,直到目標線程(pthread_t thread
)終止。如果目標線程已經終止,
pthread_join
會立即返回。如果目標線程是分離的(detached),
pthread_join
會失敗并返回錯誤碼。
示例:等待線程退出并接收返回數據
void * threadFunc(void *arg){printf("I am child thread, tid = %lu\n",pthread_self());//pthread_exit(NULL);//相當于返回成一個8字節的0char *str = "thread_exit\n";pthread_exit((void *)str);//char *tret = (char *)malloc(20);//strcpy(tret,"hello");//return (void *)tret;
}
int main(){pthread_t tid;int ret = pthread_create(&tid, NULL, threadFunc,NULL);THREAD_ERROR_CHECK(ret, "pthread_create");printf("I am main thread, tid = %lu\n", pthread_self());void *tret;//在調用函數中申請void*變量ret = pthread_join(tid, &tret);//傳入void*變量的地址THREAD_ERROR_CHECK(ret, "pthread_join");//printf("tret = %ld\n", (long) tret);printf("tret = %s\n", (char *)tret); //會輸出 thread_exit\nreturn 0;
}
注意:
在使用 pthread_join 要特別注意參數的類型, 第一個 thread 參數是不需要取地址的,如果參數錯誤,有些情況下可能會陷入無限等待,還有些情況會立即終止,觸發報錯。
ret = pthread_join(tid, &tret);//第一個 thread 參數不需要取地址
線程的取消和資源清理?
線程的取消
線程除了可以主動退出以外,還可以被另一個線程終止。不過首先值得注意的是,我們不能輕易地在多線程程序使用信號,因為多線程是共享代碼段的,從而信號處理的回調函數也是共享的,當產生一個信號到進程時,進程中用于遞送信號的線程是隨機的,很有可能會出現主線程因遞送信號而終止從而導致所有線程異常退出的情況。
pthread_cancel
pthread_cancel
是 POSIX 線程庫(pthread)中的一個函數,用于請求取消(終止)一個正在運行的線程。被取消的線程會清理其資源并退出。
#include <pthread.h>int pthread_cancel(pthread_t thread);
參數說明
pthread_t thread
指定要取消的線程的 ID。這個 ID 是通過pthread_create
創建線程時返回的。
返回值
成功時返回
0
。失敗時返回錯誤碼(非零值),例如:
ESRCH
:指定的線程 ID 無效或線程不存在。EINVAL
:指定的線程 ID 不是當前線程或線程已經終止。
功能
pthread_cancel
會向指定的線程發送一個取消請求。線程是否會被取消取決于線程的取消狀態和取消類型:
取消狀態:線程可以通過
pthread_setcancelstate
設置其取消狀態為啟用(PTHREAD_CANCEL_ENABLE
,默認值)或禁用(PTHREAD_CANCEL_DISABLE
)。取消類型:線程可以通過
pthread_setcanceltype
設置其取消類型為延遲(PTHREAD_CANCEL_DEFERRED
,默認值)或立即(PTHREAD_CANCEL_ASYNCHRONOUS
)。延遲取消:線程會在下一次調用可取消的函數(如
pthread_join
、pthread_testcancel
等)時響應取消請求。立即取消:線程會立即響應取消請求。
要實現線程的有序退出,線程內部需要實現一種不依賴于信號的機制,這就是線程的取消
pthread_cancel 的工作職責。當一個線程調用 pthread_cancel 去取消另一個線程的時候,另一個線程會將本線程的取消標志位設置為真,此時線程還不會立即取消,當這個線程執行到一些特定的函數之時,線程才會退出。這些會導致已取消未終止的線程終止的函數稱為取消點。
在 Linux 中,線程取消點(Cancellation Point)是指線程在執行過程中可以被取消的點。當線程處于取消點時,如果線程的取消狀態被設置為允許取消(PTHREAD_CANCEL_ENABLE
),并且取消類型為延遲取消(PTHREAD_CANCEL_DEFERRED
),那么線程可能會在這些點被取消。
以下是一些常見的 Linux 取消點函數,以表格形式列出:
函數名稱 | 功能描述 |
---|---|
read() | 從文件描述符讀取數據 |
write() | 向文件描述符寫入數據 |
open() | 打開文件 |
close() | 關閉文件描述符 |
wait() | 等待子進程結束 |
waitpid() | 等待指定子進程結束 |
waitid() | 等待子進程結束(POSIX 標準) |
recv() | 接收網絡數據 |
recvfrom() | 接收網絡數據(帶源地址) |
recvmsg() | 接收網絡數據(帶附加信息) |
send() | 發送網絡數據 |
sendto() | 發送網絡數據(帶目標地址) |
sendmsg() | 發送網絡數據(帶附加信息) |
accept() | 接受網絡連接 |
connect() | 建立網絡連接 |
select() | 監聽多個文件描述符(I/O 多路復用) |
poll() | 監聽多個文件描述符(I/O 多路復用) |
epoll_wait() | 等待?epoll ?文件描述符的事件 |
usleep() | 使線程休眠指定微秒數 |
nanosleep() | 使線程休眠指定納秒數 |
pause() | 使線程暫停,直到接收到信號 |
readv() | 從文件描述符讀取數據到多個緩沖區 |
writev() | 向文件描述符寫入數據從多個緩沖區 |
fread() | 從文件流讀取數據 |
fwrite() | 向文件流寫入數據 |
fscanf() | 從文件流讀取格式化數據 |
fprintf() | 向文件流寫入格式化數據 |
fgets() | 從文件流讀取一行 |
fputs() | 向文件流寫入一行 |
fgetc() | 從文件流讀取一個字符 |
fputc() | 向文件流寫入一個字符 |
fgetpos() | 獲取文件流的當前位置 |
fsetpos() | 設置文件流的當前位置 |
fseek() | 設置文件流的偏移量 |
ftell() | 獲取文件流的當前偏移量 |
rewind() | 重置文件流到文件開頭 |
fopen() | 打開文件流 |
fclose() | 關閉文件流 |
flockfile() | 鎖定文件流 |
funlockfile() | 解鎖文件流 |
ftrylockfile() | 嘗試鎖定文件流 |
pthread_join() | 等待線程結束 |
pthread_cond_wait() | 等待條件變量 |
pthread_cond_timedwait() | 等待條件變量(帶超時) |
sem_wait() | 等待信號量 |
sem_timedwait() | 等待信號量(帶超時) |
sem_trywait() | 嘗試等待信號量 |
mq_receive() | 接收消息隊列消息 |
mq_send() | 發送消息隊列消息 |
mq_timedreceive() | 接收消息隊列消息(帶超時) |
mq_timedsend() | 發送消息隊列消息(帶超時) |
示例:在沒有取消點的情況下,主線程命令子線程退出
void *threadFunc(){ //這里為了顯示子線程狀態,添加printf可能會導致子線程被取消,但目前還沒有遇到這種情況printf("I am child thread, tid = %lu\n", pthread_self());while (1){//sleep(1); //sleep是一個取消點函數,把它注釋掉看看顯現是否會取消}pthread_exit(NULL);
}
int main(int argc, char const *argv[])
{pthread_t tid;int ret = pthread_create(&tid, NULL, threadFunc, NULL);THREAD_ERROR_CHECK(ret, "thread create");printf("I am main thread, tid = %lu\n", pthread_self());pthread_cancel(tid); //命令線程取消pthread_join(tid, NULL);return 0;
}
執行這段代碼,可以發現子線程無法被主線程取消掉,如何讓sleep函數執行,會導致遇到sleep取消點時而取消子線程。
pthread_testcancel
如果需要手打添加取消點,可以調用 pthread_testcancel 函數。
pthread_testcancel
?用于顯式地測試當前線程是否有一個未決的取消請求。如果存在未決的取消請求,pthread_testcancel
會觸發取消操作,使線程退出。如果沒有未決的取消請求,該函數不會有任何操作。
#include <pthread.h>void pthread_testcancel(void);
功能
pthread_testcancel
用于測試當前線程是否有未決的取消請求。如果線程的取消狀態是啟用(
PTHREAD_CANCEL_ENABLE
),并且取消類型是延遲(PTHREAD_CANCEL_DEFERRED
),那么調用pthread_testcancel
會檢查是否有未決的取消請求。如果存在未決的取消請求,線程會執行取消操作,調用清理函數(如果有),并退出。
如果沒有未決的取消請求,函數不會有任何操作。
使用場景
pthread_testcancel
通常用于以下場景:
延遲取消類型:當線程的取消類型是延遲(
PTHREAD_CANCEL_DEFERRED
)時,線程不會立即響應取消請求,而是會在調用可取消的函數時響應。pthread_testcancel
是一個顯式的可取消點,可以用來主動檢查取消請求。長時間運行的循環:在長時間運行的循環中,調用
pthread_testcancel
可以確保線程能夠及時響應取消請求,避免線程在取消請求發出后仍然長時間運行。
線程資源清理
在引入線程取消之后,程序員在管理資源回收的難度上會急劇提升。為了簡化資源清理行為,線程庫引入了 pthread_cleanup_push 和 pthread_cleanup_pop 函數來管理線程主動或者被動終止時所申請資源(比如文件、堆空間、鎖等等)。
pthread_cleanup_push
pthread_cleanup_push
?用于注冊一個清理函數(cleanup handler)。當線程被取消(通過 pthread_cancel
)或調用 pthread_exit
時,注冊的清理函數會被自動調用。這有助于線程在退出時安全地清理資源,例如釋放動態分配的內存、關閉文件描述符等。
#include <pthread.h>void pthread_cleanup_push(void (*routine)(void *), void *arg);
?參數說明
void (*routine)(void *)
指定清理函數的指針。清理函數必須接受一個void *
類型的參數,并且沒有返回值。void *arg
傳遞給清理函數的參數。清理函數可以通過這個參數接收額外的信息,用于接收需要清理的數據
功能
pthread_cleanup_push
用于注冊一個清理函數。注冊的清理函數會在以下情況下被調用:
線程被取消(通過
pthread_cancel
)。線程調用
pthread_exit
。線程正常結束(通過返回值退出)。
清理函數的調用順序是后進先出(LIFO),即最后注冊的清理函數會最先被調用。
pthread_cleanup_push
和 pthread_cleanup_pop
必須成對出現。pthread_cleanup_push
用于注冊清理函數,而 pthread_cleanup_pop
用于取消注冊。pthread_cleanup_pop
的第二個參數決定是否調用清理函數。
pthread_cleanup_pop(0)
:取消注冊清理函數,但不調用它。pthread_cleanup_pop(1)
:取消注冊清理函數,并調用它。
pthread_cleanup_pop
pthread_cleanup_pop
?用于取消注冊之前通過 pthread_cleanup_push
注冊的清理函數。
#include <pthread.h>void pthread_cleanup_pop(int execute);
?參數說明
int execute
一個布爾值,用于決定是否調用清理函數:0
:取消注冊清理函數,但不調用它。1
:取消注冊清理函數,并調用它。
功能
取消注冊:
pthread_cleanup_pop
用于取消之前通過pthread_cleanup_push
注冊的清理函數。調用清理函數:根據
execute
參數的值,可以選擇是否調用清理函數。成對使用:
pthread_cleanup_push
和pthread_cleanup_pop
必須成對出現,否則會導致未定義行為。
示例:
void cleanup(void *p){free(p);printf("I am cleanup\n");
}
void * threadFunc(void *arg){void *p = malloc(100000);pthread_cleanup_push(cleanup,p);//一定要在cancel點之前pushprintf("I am child thread, tid = %lu\n",pthread_self());//pthread_exit((void *)0);//在pop之前exit,cleanup彈棧并被調用pthread_cleanup_pop(1);//在exit之后pop cleanup彈棧,如果參數為1被調用pthread_exit((void *)0);
}
POSIX要求 pthread_cleanup_push 和 pthread_cleanup_pop 必須成對出現在同一個作用域當中,主要是為了約束程序員在清理函數當中行為。下面是在 /urs/include/pthread.h 文件當中,線程清理函數的定義:
#define pthread_cleanup_push(routine, arg) \
do {
\
__pthread_cleanup_class __clframe (routine, arg)
/* Remove a cleanup handler installed by the matching pthread_cleanup_push.
If EXECUTE is non-zero, the handler function is called. */
#define pthread_cleanup_pop(execute) \
__clframe.__setdoit (execute);
} while (0)
可以發現push和pop的宏定義不是語義完全的,它們必須在同一作用域中成對出現才能使花括號成功匹配。
下面是壓入多個清理函數的例子:
void cleanup(void *p){printf("clean up, %s\n", (char *)p);
}
void * threadFunc(void *arg){pthread_cleanup_push(cleanup,(void *)"first");pthread_cleanup_push(cleanup,(void *)"second");//pthread_exit((void *)0);return (void *)0;pthread_cleanup_pop(1);pthread_cleanup_pop(1);
}
int main(){pthread_t tid;int ret;ret = pthread_create(&tid,NULL,threadFunc,NULL);THREAD_ERROR_CHECK(ret,"pthread_create");ret = pthread_join(tid,NULL);THREAD_ERROR_CHECK(ret,"pthread_join");return 0;
}
線程的同步和互斥(重點)
由于多線程之間不存在隔離,共享同一個地址在提高運行效率的同時也給用戶帶來了巨大的困擾。在并發執行的情況下,大量的共享資源成為競爭條件,導致程序執行的結果往往和預期的內容大相徑庭,如果一個程序的結果是不正確的,那么再高的效率也毫無意義。在基于之前進程對并發的研究之上,線程庫也提供了專門用于正確地訪問共享資源的機制。
互斥鎖的基本使用
在多線程編程中,用來控制共享資源的最簡單有效也是最廣泛使用的機制就是 mutex(MUTual
EXclusion) ,即互斥鎖。鎖的數據類型是 pthread_mutex_t ,其本質是一個全局的標志位,線程可以對作原子地測試并修改,即所謂的加鎖。當一個線程持有鎖的時候,其余線程再嘗試加鎖時(包括自己再次加鎖),會使自己陷入阻塞狀態,直到鎖被持有線程解鎖才能恢復運行。所以鎖在某個時刻永遠不能被兩個線程同時持有。
創建鎖有兩種形式:直接用 PHTREAD_MUTEX_INITIALIZER 初始化一個 pthread_mutex_t 類型的變量,即靜態初始化鎖;而使用 pthread_mutex_init 函數可以動態創建一個鎖。動態創建鎖的方式更加常見。使用 pthread_mutex_destory 可以銷毀一個鎖。
pthread_mutex_t fastmutex = PTHREAD_MUTEX_INITIALIZER;
int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t
*mutexattr);
int pthread_mutex_destroy(pthread_mutex_t *mutex);
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_init
int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *mutexattr);
動態初始化互斥鎖。
pthread_mutex_t *mutex
:指向要初始化的互斥鎖的指針。const pthread_mutexattr_t *mutexattr
:指向互斥鎖屬性的指針。如果為NULL
,則使用默認屬性。
返回值
成功時返回
0
。失敗時返回錯誤碼。
pthread_mutex_destroy
int pthread_mutex_destroy(pthread_mutex_t *mutex);
銷毀互斥鎖,釋放與互斥鎖相關的資源。
pthread_mutex_t *mutex
:指向要銷毀的互斥鎖的指針。
返回值
成功時返回
0
。失敗時返回錯誤碼。
pthread_mutex_lock
int pthread_mutex_lock(pthread_mutex_t *mutex);
加鎖操作,阻塞當前線程直到獲取互斥鎖。
pthread_mutex_t *mutex
:指向要加鎖的互斥鎖的指針。
返回值
成功時返回
0
。失敗時返回錯誤碼。
如果互斥鎖已經被其他線程持有,調用線程會阻塞,直到互斥鎖被釋放。
同一線程多次調用
pthread_mutex_lock
會導致死鎖,除非使用的是遞歸鎖(通過pthread_mutexattr_settype
設置為PTHREAD_MUTEX_RECURSIVE
)。
pthread_mutex_unlock
int pthread_mutex_unlock(pthread_mutex_t *mutex);
解鎖操作,釋放互斥鎖。
pthread_mutex_t *mutex
:指向要解鎖的互斥鎖的指針。
返回值
成功時返回
0
。失敗時返回錯誤碼。
pthread_mutex_trylock
int pthread_mutex_trylock(pthread_mutex_t *mutex);
嘗試加鎖,不會阻塞。如果互斥鎖已經被其他線程持有,立即返回。
pthread_mutex_t *mutex
:指向要嘗試加鎖的互斥鎖的指針。
返回值
如果成功獲取鎖,返回
0
。如果互斥鎖已經被其他線程持有,返回
EBUSY
。其他錯誤返回相應的錯誤碼。
注意事項
pthread_mutex_trylock
不會阻塞,因此適用于需要非阻塞加鎖的場景。如果互斥鎖已經被其他線程持有,調用線程不會等待,而是立即返回。
下面是最基本的使用鎖的例子,編程規范要求:誰加鎖,誰解鎖。鎖的使用紛繁復雜,如果不按照規范行事,一方面會很容易出現錯誤,并且出錯之后很難排查,另一方面,隨意解鎖會導致代碼的可讀性極差,無法后續調整優化。
示例:
typedef struct shareRes_s{pthread_mutex_t mutex;
} shareRes_t, *pShareRes_t;
void *threadFunc(void *arg){shareRes_t *shared = (shareRes_t *)arg;pthread_mutex_lock(&shared->mutex);puts("I am child thread");pthread_mutex_unlock(&shared->mutex);
}
int main(){shareRes_t shared;pthread_t tid;pthread_mutex_init(&shared.mutex,NULL);pthread_create(&tid,NULL,threadFunc,&shared);sleep(1);pthread_join(tid,NULL);pthread_mutex_lock(&shared.mutex);puts("I am main thread");pthread_mutex_unlock(&shared.mutex);return 0;
}
使用互斥鎖訪問共享資源
typedef struct shareRes_s{pthread_mutex_t mutex;int val;
}shareRes_t, *pshareRes_t;
void *threadFunc(void *arg){pshareRes_t share = (pshareRes_t)arg;for(int i = 0; i < 100; i++){pthread_mutex_lock(&share->mutex);share->val++;pthread_mutex_unlock(&share->mutex);}pthread_exit(NULL);
}int main(int argc, char const *argv[])
{pthread_t tid;shareRes_t share;share.val = 0;int ret = pthread_mutex_init(&share.mutex, NULL);THREAD_ERROR_CHECK(ret, "pthread_mutex_init");ret = pthread_create(&tid, NULL, threadFunc, &share);THREAD_ERROR_CHECK(ret, "pthread_create");for(int i = 0; i < 100; i++){pthread_mutex_lock(&share.mutex);share.val++;pthread_mutex_unlock(&share.mutex);}ret = pthread_join(tid, NULL);THREAD_ERROR_CHECK(ret, "pthread_join");ret = pthread_mutex_destroy(&share.mutex);THREAD_ERROR_CHECK(ret, "pthread_mutex_destroy");printf("val = %d\n", share.val);return 0;
}
另外就是要注意到,使用線程互斥量的效率要比之前的進程間通信信號量機制好很多,所以實際工作中幾乎都是使用線程互斥鎖來實現互斥。
死鎖
使用互斥鎖的時候必須小心謹慎,如果是需要持續多個鎖的情況,加鎖和解鎖之間必須要維持一定的順序。即使是只有一把鎖,如果使用不當,也會導致死鎖。當一個線程持有了鎖以后,又試圖對同一把鎖加鎖時,線程會陷入死鎖狀態。
一個很簡單的例子就是:有一個蘋果和一個橘子,兩個孩子一個擁有蘋果另一個擁有橘子,但是他們都想擁有兩個水果才開吃。但彼此又不愿放棄手中的水果,最終變得僵持不下。
可以使用 pthread_mutexattr_settype 函數修改鎖的屬性,檢錯鎖在重復加鎖是會報錯,遞歸鎖或者稱為可重入鎖在重復加鎖不會死鎖時,只是會增加鎖的引用計數,解鎖時也只是減少鎖的引用計數。但是在實際工作中,如果一個設計必須依賴于遞歸鎖,那么這個設計肯定是有問題的。
pthread_mutexattr_t
用于表示互斥鎖屬性的類型。互斥鎖屬性用于在創建互斥鎖時指定其行為和特性。通過設置互斥鎖屬性,可以控制互斥鎖的類型、協議、優先級繼承等行為。
互斥鎖屬性的作用
互斥鎖屬性允許在創建互斥鎖時指定以下特性:
互斥鎖類型:普通互斥鎖、遞歸互斥鎖、錯誤檢查互斥鎖等。
互斥鎖協議:優先級繼承、優先級保護等。
互斥鎖優先級天花板:用于實時線程的優先級管理。
互斥鎖共享性:互斥鎖是否可以在多個進程之間共享。
初始化互斥鎖屬性
互斥鎖屬性需要先初始化,然后可以設置或獲取其屬性值。
#include <pthread.h>int pthread_mutexattr_init(pthread_mutexattr_t *attr);
int pthread_mutexattr_destroy(pthread_mutexattr_t *attr);
pthread_mutexattr_init
:初始化互斥鎖屬性對象。pthread_mutexattr_destroy
:銷毀互斥鎖屬性對象,釋放相關資源。
返回值
成功時返回
0
。失敗時返回錯誤碼。
設置和獲取互斥鎖屬性
以下是一些常用的設置和獲取互斥鎖屬性的函數:
設置互斥鎖類型
int pthread_mutexattr_settype(pthread_mutexattr_t *attr, int type);
int pthread_mutexattr_gettype(const pthread_mutexattr_t *attr, int *type);
const pthread_mutexattr_t *attr
:指向互斥鎖屬性對象的指針。
int *type
:用于存儲互斥鎖類型的指針。
type
:互斥鎖類型,可以是以下值之一:
PTHREAD_MUTEX_NORMAL
描述:普通互斥鎖。
行為:
同一線程多次調用
pthread_mutex_lock
會導致死鎖。如果線程嘗試解鎖一個它未持有的互斥鎖,行為未定義。
用途:適用于大多數需要互斥保護的場景,是最常用的互斥鎖類型。
PTHREAD_MUTEX_RECURSIVE
描述:遞歸互斥鎖。
行為:
同一線程可以多次加鎖同一個互斥鎖,每次加鎖必須對應一次解鎖。
互斥鎖的計數器會記錄加鎖的次數,只有當計數器歸零時,互斥鎖才會真正釋放。
如果線程嘗試解鎖一個它未持有的互斥鎖,會返回錯誤。
用途:適用于需要在遞歸函數中多次加鎖的場景。
PTHREAD_MUTEX_ERRORCHECK
描述:錯誤檢查互斥鎖。
行為:
如果線程嘗試加鎖一個它已經持有的互斥鎖,會返回錯誤。
如果線程嘗試解鎖一個它未持有的互斥鎖,會返回錯誤。
適用于調試和錯誤檢查,可以幫助檢測互斥鎖的使用錯誤。
用途:適用于開發和調試階段,幫助發現潛在的互斥鎖使用問題。
PTHREAD_MUTEX_DEFAULT
描述:默認互斥鎖類型。
行為:與
PTHREAD_MUTEX_NORMAL
相同。用途:如果不顯式設置互斥鎖類型,互斥鎖會使用默認類型。
設置互斥鎖協議
int pthread_mutexattr_setprotocol(pthread_mutexattr_t *attr, int protocol);
int pthread_mutexattr_getprotocol(const pthread_mutexattr_t *attr, int *protocol);
const pthread_mutexattr_t *attr
:指向互斥鎖屬性對象的指針。
int *protocol
:用于存儲互斥鎖協議的指針。
protocol
:互斥鎖協議,可以是以下值之一:
PTHREAD_PRIO_NONE
描述:無優先級繼承。
行為:
線程在獲取互斥鎖時,不會改變其優先級。
適用于大多數不需要優先級管理的場景。
用途:默認協議,適用于大多數互斥鎖。
PTHREAD_PRIO_INHERIT
描述:優先級繼承。
行為:
當一個低優先級線程持有互斥鎖時,如果高優先級線程嘗試獲取該互斥鎖,低優先級線程會繼承高優先級線程的優先級。
低優先級線程釋放互斥鎖后,會恢復其原始優先級。
用于避免優先級反轉問題。
用途:適用于需要避免優先級反轉的實時系統。
PTHREAD_PRIO_PROTECT
描述:優先級保護。
行為:
互斥鎖有一個固定的優先級天花板(
prioceiling
),線程在獲取互斥鎖時會提升到該優先級。互斥鎖的優先級天花板必須在初始化時設置。
適用于需要嚴格控制優先級的實時系統。
用途:適用于需要嚴格控制優先級的實時系統。
設置互斥鎖優先級天花板
互斥鎖的優先級天花板(Priority Ceiling)是與互斥鎖協議相關的一個概念,主要用于實時系統中避免優先級反轉問題。優先級天花板是一個固定的優先級值,當線程嘗試獲取互斥鎖時,線程的優先級會被提升到這個天花板值。
優先級天花板的作用
優先級天花板的主要作用是避免優先級反轉問題。優先級反轉是指一個高優先級線程被一個低優先級線程阻塞,而低優先級線程又無法及時釋放資源,導致高優先級線程無法及時運行。優先級天花板通過提升線程的優先級,確保高優先級線程能夠盡快獲取互斥鎖。
可以通過 pthread_mutexattr_setprioceiling
函數設置互斥鎖的優先級天花板:
int pthread_mutexattr_setprioceiling(pthread_mutexattr_t *attr, int prioceiling);
pthread_mutexattr_t *attr
:指向互斥鎖屬性對象的指針。int prioceiling
:優先級天花板值,通常是一個優先級值。
可以通過 pthread_mutexattr_getprioceiling
函數獲取互斥鎖的優先級天花板:
int pthread_mutexattr_getprioceiling(const pthread_mutexattr_t *attr, int *prioceiling);
const pthread_mutexattr_t *attr
:指向互斥鎖屬性對象的指針。int *prioceiling
:用于存儲優先級天花板值的指針。
優先級范圍:優先級天花板值必須在系統支持的優先級范圍內。可以通過
sched_get_priority_min
和sched_get_priority_max
獲取系統支持的優先級范圍。協議設置:優先級天花板只在
PTHREAD_PRIO_PROTECT
協議下有效。如果使用PTHREAD_PRIO_NONE
或PTHREAD_PRIO_INHERIT
協議,優先級天花板不會生效。實時調度策略:優先級天花板通常用于實時調度策略(如
SCHED_FIFO
或SCHED_RR
)。如果線程使用默認的調度策略(如SCHED_OTHER
),優先級天花板可能不會生效。線程優先級提升:當線程獲取互斥鎖時,其優先級會被提升到天花板值。釋放互斥鎖后,線程的優先級會恢復到原始值。
設置互斥鎖共享性
互斥鎖的共享性(shared attribute)決定了互斥鎖是否可以在多個進程之間共享。在 POSIX 線程庫(pthread)中,互斥鎖的共享性有兩種設置:進程內共享(PTHREAD_PROCESS_PRIVATE
)和跨進程共享(PTHREAD_PROCESS_SHARED
)。
互斥鎖的共享性決定了互斥鎖的作用范圍:
進程內共享(
PTHREAD_PROCESS_PRIVATE
):互斥鎖僅在創建它的進程內有效,不能被其他進程訪問。跨進程共享(
PTHREAD_PROCESS_SHARED
):互斥鎖可以在多個進程之間共享,多個進程可以使用同一個互斥鎖進行同步。
可以通過 pthread_mutexattr_setpshared
函數設置互斥鎖的共享性:
int pthread_mutexattr_setpshared(pthread_mutexattr_t *attr, int pshared);
pthread_mutexattr_t *attr
:指向互斥鎖屬性對象的指針。int pshared
:共享性屬性,可以是以下值之一:PTHREAD_PROCESS_PRIVATE
:互斥鎖僅在創建它的進程內有效。PTHREAD_PROCESS_SHARED
:互斥鎖可以在多個進程之間共享。
可以通過 pthread_mutexattr_getpshared
函數獲取互斥鎖的共享性:
int pthread_mutexattr_getpshared(const pthread_mutexattr_t *attr, int *pshared);
const pthread_mutexattr_t *attr
:指向互斥鎖屬性對象的指針。int *pshared
:用于存儲共享性屬性的指針。
默認共享性:如果不顯式設置互斥鎖的共享性,互斥鎖會使用默認值
PTHREAD_PROCESS_PRIVATE
,即僅在創建它的進程內有效。跨進程共享的限制:
要使用跨進程共享的互斥鎖,必須確保互斥鎖存儲在多個進程可以訪問的共享內存中(例如,使用
mmap
或shmget
創建的共享內存)。跨進程共享的互斥鎖需要更多的系統資源和同步機制,因此性能可能會略低于進程內共享的互斥鎖。
跨進程共享的用途:適用于需要在多個進程之間同步訪問共享資源的場景,例如多進程服務器或分布式系統。
現在我們演示一個遞歸鎖的例子:
typedef struct shareRes_s{pthread_mutex_t mutex;
} shareRes_t, *pShareRes_t;
void *threadFunc(void *arg){pShareRes_t p = (pShareRes_t)arg;pthread_mutex_lock(&p->mutex);puts("fifth");pthread_mutex_unlock(&p->mutex);
}
int main(){shareRes_t shared;int ret;pthread_mutexattr_t mutexattr;pthread_mutexattr_init(&mutexattr);//ret = pthread_mutexattr_settype(&mutexattr,PTHREAD_MUTEX_ERRORCHECK);ret = pthread_mutexattr_settype(&mutexattr,PTHREAD_MUTEX_RECURSIVE);THREAD_ERROR_CHECK(ret,"pthread_mutexattr_settype");pthread_t tid;pthread_create(&tid,NULL,threadFunc,(void *)&shared);pthread_mutex_init(&shared.mutex,&mutexattr);ret = pthread_mutex_lock(&shared.mutex);THREAD_ERROR_CHECK(ret,"pthread_mute_lock 1");puts("first");ret = pthread_mutex_lock(&shared.mutex);THREAD_ERROR_CHECK(ret,"pthread_mute_lock 2");puts("second");pthread_mutex_unlock(&shared.mutex);puts("third");pthread_mutex_unlock(&shared.mutex);//兩次加鎖,要有兩次解鎖puts("forth");pthread_join(tid,NULL);return 0;
}
?輸出結果:
(base) ubuntu@ubuntu:~/MyProject/Linux/thread$ ./thread6
first
second
third
forth
fifth
同步和條件變量
理論上來說,利用互斥鎖可以解決所有的同步問題,但是生產實踐之中往往會出現這樣的問題:一個線程能否執行取決于業務的狀態,而該狀態是多線程共享的,狀態的數值會隨著程序的運行不斷地變化,線程也經常在可運行和不可運行之間動態切換。假如單純使用互斥鎖來解決問題的話,就會出現大量的重復的“加鎖-檢查條件不滿足-解鎖”的行為,這樣的話,不滿足條件的線程會經常試圖占用CPU資源,上下文切換也會非常頻繁。
對于這樣依賴于共享資源這種條件來控制線程之間的同步的問題,我們希望采用一種無競爭的方式讓多個線程在共享資源處會和——這就是條件變量。當涉及到同步問題時,業務上需要設計一個狀態/條件(一般是一個標志位)來表示該線程到底是可運行還是不可運行的,這個狀態是多線程共享的,故修改的時候必須加鎖訪問,這就意味著條件變量一定要配合鎖來使用。條件變量是一種“等待-喚醒”機制:線程運行的時候發現不滿足執行的狀態可以等待,線程運行的時候如果修改了狀態可以喚醒其他線程。
接下來我們來舉一個條件變量的例子。
業務場景:假設有兩個線程t1和t2并發運行,t1會執行A事件,t2會執行B事件,現在業務要求,無論t1或t2哪個線程先占用CPU,總是需要滿足A先B后的同步順序。
解決方案:
- 在t1和t2線程執行之前,先初始化狀態為B不可執行。
- t1線程執行A事件,執行完成以后先加鎖,修改狀態為B可執行,并喚醒條件變量,然后解鎖(解鎖和喚醒這兩個操作可以交換順序);
- t2線程先加鎖,然后檢查狀態:假如B不可執行,則B阻塞在條件變量上,當t2阻塞在條件變量以后,t2線程會解鎖并陷入阻塞狀態直到t1線程喚醒為止,t2被喚醒以后,會先加鎖。接下來t2線程處于加鎖狀態,可以在解鎖之后,再來執行B事件;而假如狀態為B可執行,則t2線程處于加鎖狀態繼續執行后續代碼,也可以在解鎖之后,再來執行B事件。
?通過上面的設計,可以保證無論t1和t2按照什么樣的順序交織執行,A事件總是先完成,B事件總是后完成——即使是比較極端的情況也是如此:比如t1一直占用CPU直到結束,那么t2占用CPU時,狀態一定是B可執行,則不會陷入等待;又比如t2先一直占用CPU,t2檢查狀態時會發現狀態為B不可執行,就會阻塞在條件變量之上,這樣就要等到A執行完成以后,才能醒來繼續運行。
下面是具體條件變量相關接口:
pthread_cond_t
pthread_cond_t
是用于表示條件變量的類型,而 PTHREAD_COND_INITIALIZER
是一個宏,用于靜態初始化條件變量。這種初始化方式適用于全局或靜態存儲期的條件變量。
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
pthread_cond_t
:條件變量的類型。PTHREAD_COND_INITIALIZER
:用于靜態初始化條件變量的宏。
pthread_cond_init
如果條件變量是動態分配的或需要非默認屬性,可以使用 pthread_cond_init
函數動態初始化條件變量。
#include <pthread.h>int pthread_cond_init(pthread_cond_t *cond, const pthread_condattr_t *cond_attr);
pthread_cond_t *cond
:指向要初始化的條件變量的指針。const pthread_condattr_t *cond_attr
:指向條件變量屬性的指針。如果為NULL
,則使用默認屬性。
返回值
成功時返回
0
。失敗時返回錯誤碼。
pthread_cond_signal
pthread_cond_signal
?用于喚醒一個正在等待該條件變量的線程。
#include <pthread.h>int pthread_cond_signal(pthread_cond_t *cond);
參數說明
pthread_cond_t *cond
:指向要發送信號的條件變量的指針。
功能
pthread_cond_signal
用于喚醒一個正在等待指定條件變量的線程。如果有多個線程正在等待同一個條件變量,
pthread_cond_signal
只會喚醒其中一個線程。如果沒有線程正在等待該條件變量,
pthread_cond_signal
不會做任何操作。
返回值
成功時返回
0
。失敗時返回錯誤碼:
EINVAL
:指定的條件變量無效。
注意事項
互斥鎖的使用:
pthread_cond_signal
通常與互斥鎖一起使用,以確保線程安全。在調用pthread_cond_signal
之前,必須先鎖定互斥鎖。喚醒單個線程:
pthread_cond_signal
只會喚醒一個等待條件變量的線程。如果有多個線程在等待,只有一個線程會被喚醒。避免競態條件:在調用
pthread_cond_signal
之前,必須確保條件已經滿足,否則可能會導致競態條件。線程安全:
pthread_cond_signal
是線程安全的,但需要確保互斥鎖的正確使用。
?pthread_cond_broadcast
pthread_cond_broadcast
?用于喚醒所有正在等待某個條件變量的線程。與 pthread_cond_signal
不同,pthread_cond_broadcast
會喚醒所有等待該條件變量的線程,而不僅僅是其中一個。
#include <pthread.h>int pthread_cond_broadcast(pthread_cond_t *cond);
參數說明
pthread_cond_t *cond
:指向要發送廣播信號的條件變量的指針。
功能
pthread_cond_broadcast
用于喚醒所有正在等待指定條件變量的線程。如果沒有線程正在等待該條件變量,
pthread_cond_broadcast
不會做任何操作。
返回值
成功時返回
0
。失敗時返回錯誤碼:
EINVAL
:指定的條件變量無效。
?pthread_cond_wait
pthread_cond_wait
?用于使當前線程等待某個條件變量被喚醒。它通常與互斥鎖一起使用,以確保線程安全。以下是 pthread_cond_wait
的詳細使用方法:
#include <pthread.h>int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);
參數說明
pthread_cond_t *cond
:指向條件變量的指針。pthread_mutex_t *mutex
:指向互斥鎖的指針。這個互斥鎖必須在調用pthread_cond_wait
之前被當前線程鎖定。
功能
pthread_cond_wait
使當前線程等待條件變量cond
被喚醒。在調用
pthread_cond_wait
時,當前線程必須已經鎖定了互斥鎖mutex
。當線程調用
pthread_cond_wait
?后,互斥鎖會被原子性地解鎖,線程進入等待狀態。當條件變量被喚醒(通過
pthread_cond_signal
或pthread_cond_broadcast
)時,線程會被喚醒,并重新鎖定互斥鎖。如果條件變量沒有被喚醒,線程會一直等待。
返回值
成功時返回
0
。失敗時返回錯誤碼:
EINVAL
:指定的條件變量或互斥鎖無效。EDEADLK
:互斥鎖沒有被當前線程鎖定。
?pthread_cond_timewait
pthread_cond_timedwait
?與 pthread_cond_wait
類似,但允許線程在等待條件變量時設置一個超時時間。如果在指定的時間內條件變量沒有被喚醒,線程將從等待狀態中退出。這使得 pthread_cond_timedwait
非常適合需要超時機制的場景。
#include <pthread.h>
#include <time.h>int pthread_cond_timedwait(pthread_cond_t *cond, pthread_mutex_t *mutex, const struct timespec *abstime);
參數說明
pthread_cond_t *cond
:指向條件變量的指針。pthread_mutex_t *mutex
:指向互斥鎖的指針。這個互斥鎖必須在調用pthread_cond_timedwait
之前被當前線程鎖定。const struct timespec *abstime
:指向絕對超時時間的指針。abstime
是一個struct timespec
類型的變量,表示從 1970 年 1 月 1 日 00:00:00 UTC 開始的絕對時間。
功能
pthread_cond_timedwait
使當前線程等待條件變量cond
被喚醒,或者直到指定的超時時間abstime
到來。在調用
pthread_cond_timedwait
時,當前線程必須已經鎖定了互斥鎖mutex
。當線程調用
pthread_cond_timedwait
時,互斥鎖會被原子性地解鎖,線程進入等待狀態。如果條件變量在超時時間內被喚醒(通過
pthread_cond_signal
或pthread_cond_broadcast
),線程會被喚醒,并重新鎖定互斥鎖。如果超時時間到達,線程也會從等待狀態中退出,并重新鎖定互斥鎖。
返回值
成功時返回
0
。失敗時返回錯誤碼:
ETIMEDOUT
:超時時間到達,線程從等待狀態中退出。EINVAL
:指定的條件變量或互斥鎖無效。EDEADLK
:互斥鎖沒有被當前線程鎖定。EINVAL
:abstime
指定的時間無效(例如,tv_nsec
超出范圍)。
?pthread_cond_destroy
pthread_cond_destroy
?用于釋放與條件變量相關的資源,確保條件變量在不再使用時被正確清理。
#include <pthread.h>int pthread_cond_destroy(pthread_cond_t *cond);
參數說明
pthread_cond_t *cond
:指向要銷毀的條件變量的指針。
功能
pthread_cond_destroy
用于銷毀條件變量,釋放與條件變量相關的資源。在銷毀條件變量之前,必須確保沒有線程正在等待該條件變量。否則,行為未定義,可能會導致程序崩潰或其他未定義行為。
返回值
成功時返回
0
。失敗時返回錯誤碼:
EBUSY
:有線程正在等待該條件變量。EINVAL
:指定的條件變量無效。
現在我們對之前A和B的同步事件進行實現:
typedef struct shareRes_s{int flag;pthread_mutex_t mutex;pthread_cond_t cond;}shareRes_t;
void A(){printf("Before A\n");sleep(3);printf("After A\n");
}
void B(){printf("Before B\n");sleep(3);printf("After B\n");
}
void *threadFunc(void *arg){sleep(3);shareRes_t * pshareRes = (shareRes_t *)arg;pthread_mutex_lock(&pshareRes->mutex);if(pshareRes->flag != 1){pthread_cond_wait(&pshareRes->cond, &pshareRes->mutex);}pthread_mutex_unlock(&pshareRes->mutex);B();pthread_exit(NULL);
}
int main(int argc, char const *argv[])
{shareRes_t shareRes;shareRes.flag = 0;pthread_t tid;pthread_mutex_init(&shareRes.mutex,NULL);pthread_cond_init(&shareRes.cond, NULL);pthread_create(&tid, NULL, threadFunc, &shareRes);A();pthread_mutex_lock(&shareRes.mutex);shareRes.flag = 1;pthread_cond_signal(&shareRes.cond);pthread_mutex_unlock(&shareRes.mutex);pthread_join(tid, NULL);return 0;
}
實戰:利用同步和互斥實現售票放票功能
typedef struct shareRes_s{int val;pthread_mutex_t mutex;pthread_cond_t cond;
}shareRes_t;void *SellTicket(void *arg){sleep(1);shareRes_t *pshareRes = (shareRes_t *)arg;while (1){pthread_mutex_lock(&pshareRes->mutex);if(pshareRes->val > 0){printf("Before 1 sells tickets, ticketNum = %d\n", pshareRes->val);pshareRes->val--;if(pshareRes->val == 0){pthread_cond_signal(&pshareRes->cond);}usleep(500000);printf("After 1 sells tickets, ticketNum = %d\n", pshareRes->val);pthread_mutex_unlock(&pshareRes->mutex);usleep(200000);//等待一會,讓setTicket搶到鎖}else{pthread_mutex_unlock(&pshareRes->mutex);break;}}pthread_exit(NULL);}
void *ProduceTicket(void *arg){shareRes_t *pshareRes = (shareRes_t *)arg;pthread_mutex_lock(&pshareRes->mutex);if(pshareRes->val > 0){printf("set is waiting\n");int ret = pthread_cond_wait(&pshareRes->cond,&pshareRes->mutex);THREAD_ERROR_CHECK(ret, "pthread_cond_wait");}printf("add tickets\n");pshareRes->val = 10;pthread_mutex_unlock(&pshareRes->mutex);pthread_exit(NULL);
}
int main(int argc, char const *argv[])
{shareRes_t share;share.val = 20;int ret = pthread_mutex_init(&share.mutex, NULL);THREAD_ERROR_CHECK(ret,"pthread_mutex_init");ret = pthread_cond_init(&share.cond, NULL);THREAD_ERROR_CHECK(ret,"pthread_cond_init");pthread_t tidS;pthread_t tidP;ret = pthread_create(&tidS, NULL, SellTicket, &share);THREAD_ERROR_CHECK(ret,"pthread_create_tids");pthread_create(&tidP, NULL, ProduceTicket, &share);THREAD_ERROR_CHECK(ret,"pthread_create_tidp");pthread_join(tidS, NULL);pthread_join(tidP, NULL);pthread_cond_destroy(&share.cond);pthread_cond_destroy(&share.cond);return 0;
}
輸出結果:
(base) ubuntu@ubuntu:~/MyProject/Linux/thread$ ./thread8
set is waiting
Before 1 sells tickets, ticketNum = 20
After 1 sells tickets, ticketNum = 19
Before 1 sells tickets, ticketNum = 19
After 1 sells tickets, ticketNum = 18
Before 1 sells tickets, ticketNum = 18
........
After 1 sells tickets, ticketNum = 1
Before 1 sells tickets, ticketNum = 1
After 1 sells tickets, ticketNum = 0
add tickets
Before 1 sells tickets, ticketNum = 10
After 1 sells tickets, ticketNum = 9
Before 1 sells tickets, ticketNum = 9
After 1 sells tickets, ticketNum = 8
Before 1 sells tickets, ticketNum = 8
........
After 1 sells tickets, ticketNum = 1
Before 1 sells tickets, ticketNum = 1
After 1 sells tickets, ticketNum = 0
?實戰:利用互斥鎖和條件變量實現簡單的生產者和消費者問題
typedef struct shareRes_s{int val;pthread_mutex_t mutex;pthread_cond_t full;pthread_cond_t empty;
}shareRes_t;
void *Consumer(void *arg){shareRes_t *pshare = (shareRes_t *)arg;while (1){pthread_mutex_lock(&pshare->mutex);if(pshare->val > 0){pshare->val--;printf("consume a product current is %d\n",pshare->val);pthread_cond_signal(&pshare->empty);}else{printf("wait to produce current is %d\n",pshare->val);pthread_cond_wait(&pshare->full, &pshare->mutex);}pthread_mutex_unlock(&pshare->mutex);usleep(200000);}pthread_exit(NULL);
}
void *Producer(void *arg){shareRes_t *pshare = (shareRes_t *)arg;while (1){pthread_mutex_lock(&pshare->mutex);if(pshare->val < 5){pshare->val++;printf("produce a product current is %d\n",pshare->val);pthread_cond_signal(&pshare->full);}else{printf("wait to consume current is %d\n",pshare->val);pthread_cond_wait(&pshare->empty, &pshare->mutex);}pthread_mutex_unlock(&pshare->mutex);usleep(500000);}pthread_exit(NULL);
}
int main(int argc, char const *argv[])
{shareRes_t share;pthread_mutex_init(&share.mutex, NULL);pthread_cond_init(&share.empty, NULL);pthread_cond_init(&share.empty, NULL);share.val = 5;pthread_t tidc, tidp;pthread_create(&tidc, NULL, Consumer, &share);pthread_create(&tidp, NULL, Producer, &share);pthread_join(tidc, NULL);pthread_join(tidp, NULL);return 0;
}
?線程的屬性
在線程創建的時候,用戶可以給線程指定一些屬性,用來控制線程的調度情況、CPU綁定情況、屏障、線程調用棧和線程分離等屬性。這些屬性可以通過一個 pthread_attr_t 類型的變量來控制,可以使用pthread_attr_set 系列設置屬性,然后可以傳入 pthread_create 函數,從控制新建線程的屬性。
pthread_attr_init
初始化線程屬性對象。
int pthread_attr_init(pthread_attr_t *attr);
pthread_attr_t *attr
:指向線程屬性對象的指針。返回值:成功時返回
0
,失敗時返回錯誤碼。
pthread_attr_destroy
銷毀線程屬性對象,釋放相關資源。
int pthread_attr_destroy(pthread_attr_t *attr);
pthread_attr_t *attr
:指向線程屬性對象的指針。返回值:成功時返回
0
,失敗時返回錯誤碼。
pthread_attr_setdetachstate
設置線程的分離狀態。
int pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate);
pthread_attr_t *attr
:指向線程屬性對象的指針。int detachstate
:線程的分離狀態,可以是PTHREAD_CREATE_DETACHED
或PTHREAD_CREATE_JOINABLE
。返回值:成功時返回
0
,失敗時返回錯誤碼。
pthread_attr_setstacksize
設置線程的堆棧大小。
int pthread_attr_setstacksize(pthread_attr_t *attr, size_t stacksize);
pthread_attr_t *attr
:指向線程屬性對象的指針。size_t stacksize
:線程的堆棧大小。返回值:成功時返回
0
,失敗時返回錯誤碼。
pthread_attr_setstack
設置線程的堆棧地址和大小。
int pthread_attr_setstack(pthread_attr_t *attr, void *stackaddr, size_t stacksize);
pthread_attr_t *attr
:指向線程屬性對象的指針。void *stackaddr
:堆棧的起始地址。size_t stacksize
:堆棧的大小。返回值:成功時返回
0
,失敗時返回錯誤碼。
pthread_attr_setschedpolicy
設置線程的調度策略。
int pthread_attr_setschedpolicy(pthread_attr_t *attr, int policy);
pthread_attr_t *attr
:指向線程屬性對象的指針。int policy
:調度策略,可以是SCHED_FIFO
、SCHED_RR
或SCHED_OTHER
。返回值:成功時返回
0
,失敗時返回錯誤碼。
pthread_attr_setschedparam
設置線程的調度參數。
int pthread_attr_setschedparam(pthread_attr_t *attr, const struct sched_param *param);
pthread_attr_t *attr
:指向線程屬性對象的指針。const struct sched_param *param
:指向調度參數的指針。返回值:成功時返回
0
,失敗時返回錯誤碼。
pthread_attr_setguardsize
設置線程的棧保護大小。
int pthread_attr_setguardsize(pthread_attr_t *attr, size_t guardsize);
pthread_attr_t *attr
:指向線程屬性對象的指針。size_t guardsize
:棧保護大小。返回值:成功時返回
0
,失敗時返回錯誤碼。
pthread_attr_setinheritsched
設置線程的調度屬性繼承方式。
int pthread_attr_setinheritsched(pthread_attr_t *attr, int inherit);
pthread_attr_t *attr
:指向線程屬性對象的指針。int inherit
:調度屬性繼承方式,可以是PTHREAD_INHERIT_SCHED
或PTHREAD_EXPLICIT_SCHED
。返回值:成功時返回
0
,失敗時返回錯誤碼。
線程安全與可重入性
線程安全
線程安全是指一個函數、類或對象在多線程環境下被并發調用時,能夠正確地處理多個線程之間的共享數據,不會出現數據競爭(Data Race)、數據不一致或程序崩潰等問題。換句話說,線程安全的代碼在多線程環境中可以被多個線程同時調用,而不會導致意外的錯誤。
線程安全的代碼:在多線程環境下,即使多個線程同時訪問和修改共享數據,代碼也能保證數據的完整性和正確性。例如,一個線程安全的計數器函數,無論多少個線程同時調用它,計數器的值始終是正確的。
線程不安全的代碼:在多線程環境下,可能會出現數據競爭,導致數據被錯誤地修改或覆蓋,從而引發程序錯誤。例如,一個簡單的全局變量計數器,如果多個線程同時對其進行自增操作,可能會出現計數器的值不符合預期的情況。
實現方式
互斥鎖(Mutex)
互斥鎖是一種最常用的線程同步機制,用于保護共享數據,確保同一時間只有一個線程可以訪問該數據。在 Linux C 中,可以使用 POSIX 線程庫(pthread)中的互斥鎖來實現線程安全。
#include <pthread.h>pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
int count = 0;void* increment(void* arg) {pthread_mutex_lock(&lock);count++;pthread_mutex_unlock(&lock);return NULL;
}
在上面的代碼中,pthread_mutex_lock
和 pthread_mutex_unlock
用于鎖定和解鎖互斥鎖,確保 count++
操作是線程安全的。
信號量(Semaphore)
信號量是一種更高級的同步機制,用于控制多個線程對共享資源的訪問。信號量可以用來限制同時訪問共享資源的線程數量。在 Linux 中,信號量可以通過 sem_t
類型實現。
#include <semaphore.h>sem_t sem;
int count = 0;void* increment(void* arg) {sem_wait(&sem);count++;sem_post(&sem);return NULL;
}
原子操作(Atomic Operations)
原子操作是指不可分割的操作,即在執行過程中不會被其他線程中斷。Linux 提供了一些原子操作的函數,例如 __sync_add_and_fetch
和 __sync_sub_and_fetch
,這些函數可以用于實現線程安全的計數器。
#include <stdatomic.h>atomic_int count = 0;void* increment(void* arg) {atomic_fetch_add(&count, 1);return NULL;
}
在上面的代碼中,atomic_fetch_add
是一個原子操作函數,用于安全地對 count
進行自增操作。
線程局部存儲(Thread Local Storage)
線程局部存儲是一種機制,用于為每個線程分配獨立的變量副本,從而避免線程之間的數據共享。在 Linux C 中,可以使用 __thread
關鍵字或 pthread_key_create
函數來實現線程局部存儲。
__thread int count = 0;void* increment(void* arg) {count++;return NULL;
}
在上面的代碼中,每個線程都有自己的 count
變量副本,因此不需要進行線程同步。
注意事項
避免全局變量:盡量減少全局變量的使用,因為全局變量是線程之間共享的,容易引發線程安全問題。
使用線程同步機制:在需要共享數據時,使用互斥鎖、信號量、原子操作等線程同步機制來保護數據。
減少鎖的粒度:盡量減少鎖的范圍,只在需要保護的代碼段中使用鎖,以提高程序的性能。
避免死鎖:在使用多個鎖時,要注意鎖的順序,避免死鎖的發生。
可重入性
可重入性指的是一個函數或模塊在被多次調用時,能夠正確地處理多次調用之間的狀態,不會因為多次調用而導致數據錯誤或程序崩潰。換句話說,一個可重入的函數在被多次調用時,每次調用都是獨立的,不會相互干擾。
一個可重入的函數必須滿足以下條件:
不依賴于全局變量或靜態變量:如果函數依賴于全局變量或靜態變量,那么在多次調用時可能會因為這些變量的狀態不一致而導致錯誤。可重入函數通常使用局部變量或通過參數傳遞數據。
不調用不可重入的函數:如果一個函數調用了不可重入的函數,那么它本身也不可重入。例如,
strtok
函數是不可重入的,因為它依賴于全局變量。不修改輸入參數:如果函數修改了輸入參數,那么在多次調用時可能會導致錯誤。可重入函數通常不會修改輸入參數,或者會通過返回值傳遞結果。
不使用靜態或全局數據結構:如果函數使用了靜態或全局數據結構,那么在多次調用時可能會導致沖突。可重入函數通常使用動態分配的數據結構或局部變量。
一個比較典型的不可重入函數例子就是 malloc 函數, malloc 函數必然是要修改靜態數據的,為了保證線程安全性, malloc 函數的實現當中會存在加鎖和解鎖的過程,假如 malloc 執行到加鎖之后,解鎖之前的時候,此時有信號產生并且遞送的話,線程會轉向執行信號處理回調函數,假如信號處理函數當中又調用了 malloc 函數,此時就會導致死鎖——這就是 malloc 的不可重入性。
不可重入函數示例:
#include <stdio.h>
#include <string.h>char* strtok_example(char* str, const char* delim) {static char* last = NULL;if (str != NULL) {last = str;}if (last == NULL) {return NULL;}char* start = last;char* end = strpbrk(last, delim);if (end != NULL) {*end = '\0';last = end + 1;} else {last = NULL;}return start;
}int main() {char str1[] = "hello,world";char str2[] = "foo,bar";printf("%s\n", strtok_example(str1, ","));printf("%s\n", strtok_example(str2, ","));printf("%s\n", strtok_example(NULL, ","));printf("%s\n", strtok_example(NULL, ","));return 0;
}
在上面的代碼中,strtok_example
函數使用了靜態變量 last
,因此它是不可重入的。如果在多線程環境中調用這個函數,可能會導致數據錯誤。