Linux C 多線程基本操作

我們已經了解進程的基本概念:進程是正在執行的程序,并且是系統資源分配的基本單位。當用戶需要在一臺計算機上去完成多個獨立的工作任務時,可以使用多進程的方式,為每個獨立的工作任務分配一個進程。多進程的管理則由操作系統負責——操作系統調度進程,合理地在多個進程之間分配資源,包括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獲取線程idgetpid

在使用線程相關的函數之后,在鏈接時需要加上 -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_joinpthread_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_pushpthread_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_pushpthread_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_minsched_get_priority_max 獲取系統支持的優先級范圍。

  • 協議設置:優先級天花板只在 PTHREAD_PRIO_PROTECT 協議下有效。如果使用 PTHREAD_PRIO_NONEPTHREAD_PRIO_INHERIT 協議,優先級天花板不會生效。

  • 實時調度策略:優先級天花板通常用于實時調度策略(如 SCHED_FIFOSCHED_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,即僅在創建它的進程內有效。

  • 跨進程共享的限制

    • 要使用跨進程共享的互斥鎖,必須確保互斥鎖存儲在多個進程可以訪問的共享內存中(例如,使用 mmapshmget 創建的共享內存)。

    • 跨進程共享的互斥鎖需要更多的系統資源和同步機制,因此性能可能會略低于進程內共享的互斥鎖。

  • 跨進程共享的用途:適用于需要在多個進程之間同步訪問共享資源的場景,例如多進程服務器或分布式系統。

現在我們演示一個遞歸鎖的例子:

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_signalpthread_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_signalpthread_cond_broadcast),線程會被喚醒,并重新鎖定互斥鎖。

  • 如果超時時間到達,線程也會從等待狀態中退出,并重新鎖定互斥鎖。

返回值

  • 成功時返回 0

  • 失敗時返回錯誤碼:

    • ETIMEDOUT:超時時間到達,線程從等待狀態中退出。

    • EINVAL:指定的條件變量或互斥鎖無效。

    • EDEADLK:互斥鎖沒有被當前線程鎖定。

    • EINVALabstime 指定的時間無效(例如,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_DETACHEDPTHREAD_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_FIFOSCHED_RRSCHED_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_SCHEDPTHREAD_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_lockpthread_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,因此它是不可重入的。如果在多線程環境中調用這個函數,可能會導致數據錯誤。

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/915551.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/915551.shtml
英文地址,請注明出處:http://en.pswp.cn/news/915551.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

C語言基礎:二維數組練習題

1. 一個二維數組賦了初值&#xff0c;用戶輸入一個數&#xff0c;在該二維數組中查找。找到則返回行列位置&#xff0c;沒找到則提示。#include <stdio.h>int main() {int arr[3][3] {{1, 2, 3},{4, 5, 6},{7, 8, 9}};int t;printf("要查找的數&#xff1a;")…

Java面試題034:一文深入了解MySQL(6)

Java面試題029&#xff1a;一文深入了解MySQL&#xff08;1&#xff09; Java面試題030&#xff1a;一文深入了解MySQL&#xff08;2&#xff09; Java面試題031&#xff1a;一文深入了解MySQL&#xff08;3&#xff09; Java面試題032&#xff1a;一文深入了解MySQL&#x…

Java基礎教程(011):面向對象中的構造方法

10-面向對象-構造方法 構造方法也叫做構造器、構造函數。 作用&#xff1a;在創建對象的時候給成員變量進行初始化的。 ? 一、構造方法的特點特點說明與類同名構造方法的名稱必須與類名相同沒有返回類型構造方法沒有返回值&#xff0c;甚至不能寫 void自動調用使用 new 創建對…

Adobe Photoshop:數字圖像處理的終極工具指南

Hi&#xff0c;我是布蘭妮甜 &#xff01;Adobe Photoshop自1990年問世以來&#xff0c;已經成為數字圖像處理領域的標桿和代名詞。這款強大的軟件不僅徹底改變了攝影、設計和藝術創作的方式&#xff0c;還深刻影響了我們消費和感知視覺內容的文化方式。從專業攝影師到社交媒體…

本期來講講什么是LVS集群?

集群和分布式 集群&#xff08;Cluster&#xff09;&#xff0c;解決某個問題將多臺計算機組合形成的系統群。 常見的集群類型&#xff1a; 負載均衡(LoadBalancing&#xff0c;簡稱LB)&#xff1a;由多個相同配置的主機組成&#xff0c;每個主機經過調度承擔部分訪問&#…

JVM 類加載過程筆記

一、概述 JVM&#xff08;Java Virtual Machine&#xff09;在運行 Java 程序時&#xff0c;需要將 .class 字節碼文件加載到內存中&#xff0c;并轉換成可以被 JVM 執行的數據結構&#xff0c;這一過程就是 類加載過程&#xff08;Class Loading Process&#xff09;。 JVM 的…

基于爬蟲技術的電影數據可視化系統 Python+Django+Vue.js

本文項目編號 25002 &#xff0c;文末自助獲取源碼 \color{red}{25002&#xff0c;文末自助獲取源碼} 25002&#xff0c;文末自助獲取源碼 目錄 一、系統介紹二、系統錄屏三、啟動教程四、功能截圖五、文案資料5.1 選題背景5.2 國內外研究現狀 六、核心代碼6.1 查詢數據6.2 新…

如何用 LUKS 和 cryptsetup 為 Linux 配置加密

在信息安全愈發重要的今天&#xff0c;為 Linux 系統盤配置全盤加密已經成為很多企業和個人的選擇。LUKS&#xff08;Linux Unified Key Setup&#xff09;配合工具 cryptsetup 可以在不犧牲性能的前提下實現高強度加密。本文將通過一個故事化的場景&#xff0c;介紹整個配置過…

VIVADO技巧_BUFGMUX時序優化

1.版本說明日期作者版本說明2025xxxx風釋雪初始版本 2.概述 基于VIVADO時序約束&#xff0c;BUFGMUX多路時鐘選擇原語的設計3.原語介紹 7系列FPGA/UltraSCale/UltraSCaleBUFGMUX_CTRL BUFGMUX_CTRL_inst (.O(O), // 1-bit output: Clock output.I0(I0), // 1-bit input: Cloc…

服務器系統時間不準確怎么辦?

服務器系統時間不準確可能會導致日志錯亂、任務調度失敗、SSL證書校驗錯誤等問題。以下是解決辦法&#xff1a;&#x1f310; 一、同步系統時間的方法1. 使用 timedatectl 命令&#xff08;適用于 systemd 系統&#xff09;timedatectl set-ntp true # 開啟自動同步 timedatect…

零信任產品聯合寧盾泛終端網絡準入,打造隨需而變、精準貼合業務的網絡安全訪問體系

零信任網絡訪問控制&#xff08;Zero Trust Network Access&#xff0c;ZTNA&#xff0c;文中零信任皆指 ZTNA&#xff09;基于“永不信任&#xff0c;持續驗證”的理念&#xff0c;打破了企業基于傳統網絡邊界進行防護的固有模式。在當前日趨復雜的網絡環境下&#xff0c;內部…

【未限制消息消費導致數據庫CPU告警問題排查及解決方案】

一、背景 某天下午&#xff0c;上游系統同一時間突然下了三個大合同數據&#xff0c;平均每個合同數據實例在6萬以上的量級&#xff0c;短短幾分鐘內瞬間有20萬左右的流量涌入系統。 而在正常情況下&#xff0c;系統1天處理的流量也不過2千量級&#xff0c;當時數據庫指標監控告…

iOS開發 Swift 速記2:三種集合類型 Array Set Dictionary

初級代碼游戲的專欄介紹與文章目錄-CSDN博客 我的github&#xff1a;codetoys&#xff0c;所有代碼都將會位于ctfc庫中。已經放入庫中我會指出在庫中的位置。 這些代碼大部分以Linux為目標但部分代碼是純C的&#xff0c;可以在任何平臺上使用。 源碼指引&#xff1a;github源…

Apache基礎配置

一、Apache安裝# 安裝apache [rootwebserver ~]# yum install httpd -y# 在防火墻中放行web服務 [rootwebserver ~]# firewall-cmd --permanent --add-servicehttp success [rootwebserver ~]# firewall-cmd --permanent --add-servicehttps success# 開啟服務 [rootwebserver …

Python100個庫分享第37個—BeautifulSoup(爬蟲篇)

目錄專欄導讀&#x1f4da; 庫簡介&#x1f3af; 主要特點&#x1f6e0;? 安裝方法&#x1f680; 快速入門基本使用流程解析器選擇&#x1f50d; 核心功能詳解1. 基本查找方法find() 和 find_all()CSS選擇器2. 屬性操作3. 文本提取&#x1f577;? 實戰爬蟲案例案例1&#xff…

石子入水波紋效果:頂點擾動著色器實現

水面波紋的真實模擬是計算機圖形學中一個經典且重要的課題,廣泛應用于游戲、影視和虛擬現實等領域。本文將從技術原理和實現細節出發,系統介紹如何利用**頂點擾動(Vertex Displacement)**技術,結合多種輔助方法,打造既真實又高效的水面波紋效果。 一、頂點擾動的核心思想…

【FFmpeg 快速入門】本地播放器 項目

目錄 &#x1f308;前言&#x1f308; &#x1f4c1; 整體架構 詳細流程 &#x1f4c1; 數據流向? &#x1f4c1; 隊列設計?編輯 &#x1f4c1; 線程設計 &#x1f4c1; 音視頻同步 &#x1f4c1; 音頻輸出設計 &#x1f4c1; 視頻輸出設計 &#x1f4c1; 總結 …

Maven dependencyManagement標簽 properties標簽

dependencyManagement標簽properties標簽

前端埋坑之element Ui 組件el-progress display:flex后不顯示進度條解決方案

項目適用場景&#xff1a; <divs style"display&#xff1a;flex"> <span>這里是進度條前標題說明</span> <el-progress :percentage"50"></el-progress> </div> 問題呈現&#xff1a; el-progress進度條沒啦&#xf…