嵌入式Linux:線程的創建、終止、回收、取消和分離
線程的創建、終止、取消、回收和分離操作是多線程編程的核心。
在多線程編程中,需要妥善管理線程的生命周期,以避免資源泄漏、競爭條件或僵尸線程等問題。
1
創建線程
在 Linux 中,默認情況下,一個進程啟動時是單線程運行的,這個線程被稱為 主線程。
然而,現代計算任務通常需要并行處理,主線程可以通過 pthread_create() 函數創建額外的線程來并行執行任務。
這些額外的線程與主線程共享進程的資源(如內存空間、文件描述符等),但它們有獨立的執行路徑。
pthread_create() 函數的定義如下:
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);
函數參數:
thread:這是一個 pthread_t 類型的指針,指向存儲線程 ID 的變量。pthread_t 是用于唯一標識線程的類型,當創建線程成功時,該變量會被賦值為新線程的 ID,在后續的線程管理中使用。
attr:這是一個指向 pthread_attr_t 類型的指針,用于設置線程的屬性。如果設置為 NULL,則使用線程的默認屬性。這些屬性包括線程是否分離(detached)、棧大小等。如果需要設置特定的屬性,可以使用 pthread_attr_init() 和相關的屬性函數。
start_routine:這是線程執行函數的指針。新線程創建后,會從這個函數開始執行。該函數必須符合以下原型:
void* (*start_routine)(void *arg);
它接收一個 void* 類型的參數(arg),并返回一個 void* 類型的返回值。
arg:這是傳遞給 start_routine 函數的參數。可以是任意類型的指針。如果不需要傳遞參數,可以設置為 NULL。如果需要傳遞多個參數,可以使用結構體將它們打包后通過該指針傳入。
返回值:
成功時返回 0。
失敗時返回錯誤號,表示失敗的原因。例如,EAGAIN 表示系統資源不足無法創建新線程,EINVAL 表示傳入的屬性無效。
創建線程的關鍵點:
線程 ID: 每個線程都有唯一的 ID,用于區分線程。創建線程時,pthread_create() 會將新線程的 ID 存儲在 pthread_t 類型的變量中,便于后續操作。
線程屬性: 默認情況下,線程使用系統的默認屬性。如果需要更改線程的屬性,比如將其設置為 分離線程 或指定線程的棧大小,可以通過 pthread_attr_t 來設置。
啟動函數和參數: 新線程會從 start_routine 函數開始執行,并傳入 arg 參數。可以通過將多個參數封裝在結構體中,一并傳遞給該函數。
當一個新線程被創建后,它立即加入系統的 線程調度隊列,并在合適的時機獲取 CPU 執行時間。
由于調度是由操作系統控制的,所以無法預料新創建的線程和主線程誰會先執行。
如果程序對線程的執行順序有嚴格要求,可以使用同步機制(如 互斥鎖 或 信號量)來控制線程間的執行順序。
下面是一個創建線程并傳遞參數的簡單示例:
void* thread_function(void* arg) { int* num = (int*)arg; printf("New thread running with argument: %dn", *num); return NULL;}
int main() { pthread_t thread; int arg = 42;
// 創建線程,傳遞參數 arg if (pthread_create(&thread, NULL, thread_function, &arg) != 0) { perror("pthread_create failed"); return 1; }
// 等待新線程執行完畢 pthread_join(thread, NULL);
printf("Main thread finishedn"); return 0;}
解釋:
線程函數 thread_function() 接收一個 void* 類型的參數,并將其強制轉換為 int* 類型,打印傳入的值。
主線程 調用 pthread_create() 創建了一個新線程,并將 arg 作為參數傳遞給新線程的函數 thread_function()。
創建線程后,主線程調用 pthread_join() 等待新線程完成執行。如果不使用 pthread_join(),主線程不會等待新線程結束,這可能導致程序提前退出。
2
終止線程
在 Linux 中,終止線程可以通過多種方式完成,不同的方式影響線程的退出行為和進程的狀態管理。
我們詳細說明幾種終止線程的常用方法。
return: 當線程的 start 函數執行 return 時,線程正常終止,并返回指定的值,返回值可以通過 pthread_join() 獲取。
pthread_exit(): 線程可以通過顯式調用 pthread_exit() 來終止自身,pthread_exit() 允許線程在任何位置退出,返回的值也可以通過 pthread_join() 獲取。
pthread_cancel(): 通過 pthread_cancel() 可以請求取消一個線程,線程需要響應取消請求才能終止。
exit() 和 _exit(): 當進程中的任意線程調用 exit()、_exit() 或 _Exit() 時,整個進程,包括所有線程,都會被終止。
2.1、通過 return 語句退出線程
線程的 start 函數(即傳遞給 pthread_create() 的函數)在執行完畢時,可以直接使用 return 語句返回。這種方式會使線程正常退出,并將返回值作為線程的退出碼。
這與調用 pthread_exit() 類似。
void* thread_function(void* arg) { // 執行一些任務 int result = 42; return (void*)result; // 通過 return 語句退出線程}
在上面的代碼中,線程執行完 thread_function() 后,通過 return 返回 result,并且這個返回值可以通過 pthread_join() 函數在主線程中獲取。
2.2、通過 pthread_exit() 退出線程
pthread_exit() 是專門用于退出線程的函數,它允許線程在任何位置顯式退出,而不是依賴于 return。
調用 pthread_exit() 后,線程的控制流會立即結束,不再執行后續代碼。
pthread_exit() 函數原型:
void pthread_exit(void *retval);
參數 retval: retval 是一個 void* 類型的指針,指定線程的返回值,也就是線程的退出碼。這個值可以被其他線程通過 pthread_join() 獲取。
示例如下:
void* thread_function(void* arg) { int* retval = (int*)arg; printf("Thread exiting with value: %dn", *retval); pthread_exit(retval); // 顯式退出線程并返回值}
int main() { pthread_t thread; int arg = 42; int* retval;
pthread_create(&thread, NULL, thread_function, &arg); pthread_join(thread, (void**)&retval); // 獲取線程的退出碼
printf("Thread returned: %dn", *retval); return 0;}
解釋:
在該示例中,pthread_exit() 顯式終止了線程,并返回參數 arg 的值。
主線程通過 pthread_join() 獲取了子線程的退出碼。
2.3、通過 exit()、_exit() 或 _Exit() 終止整個進程
exit()、_exit() 和 _Exit() 不是用于終止單個線程的,而是用于終止整個進程。
由于線程共享同一個進程資源,如果任意一個線程調用這些函數,整個進程(包括所有線程)都會終止。
exit(): 正常終止進程,執行清理函數、關閉文件描述符等。
_exit() 和 _Exit(): 立即終止進程,不執行清理工作。
以下示例中,thread_function() 中調用了 exit(),導致整個進程被終止,主線程也不會繼續執行。
void* thread_function(void* arg) { printf("Thread running...n"); exit(0); // 調用 exit(),導致整個進程終止 return NULL;}
int main() { pthread_t thread;
pthread_create(&thread, NULL, thread_function, NULL); pthread_join(thread, NULL);
// 如果沒有被 exit() 終止,主線程會繼續執行這行代碼 printf("Main thread finishedn");
return 0;}
3
回收線程
在多線程編程中,當線程結束后,其占用的資源不會立即被系統釋放,除非顯式回收這些資源,否則這些線程會變成 僵尸線程。
在 Linux 中,回收線程的操作與進程的回收類似。
正如進程中的父進程可以使用 wait() 來回收子進程的資源,線程中也需要通過 pthread_join() 來回收線程資源并獲取線程的退出狀態。
pthread_join() 是用于 等待指定線程終止并回收其資源 的函數,它會阻塞調用線程直到目標線程終止。
如果線程已經終止,pthread_join() 將立即返回。
通過這個函數,主線程或其他線程可以獲取目標線程的退出狀態,并清理其占用的資源,避免產生僵尸線程。
pthread_join() 的函數原型:
int pthread_join(pthread_t thread, void **retval);
函數參數說明:
thread: 這是目標線程的線程 ID,pthread_join() 將等待這個線程終止。
retval: 這是一個指向 void* 類型的指針,指向保存線程返回值的內存地址。如果目標線程通過 pthread_exit() 或 return 語句返回了某個值,這個值將被存儲在 *retval 指向的內存中。如果 retval 為 NULL,則表示不關心目標線程的返回值。
返回值:
成功時返回 0。
如果調用失敗,pthread_join() 將返回一個錯誤碼。例如,ESRCH 表示指定的線程不存在,EINVAL 表示線程不可被 pthread_join() 回收,或者調用線程嘗試等待自身終止。
以下例子中,線程執行完 thread_function() 后通過 pthread_exit() 返回 result。
主線程調用 pthread_join() 等待線程結束,并成功獲取到了線程的返回值。
void* thread_function(void* arg) { int result = 100; printf("Thread running...n"); pthread_exit((void*)&result); // 顯式返回一個結果}
int main() { pthread_t thread; int* thread_result;
// 創建線程 pthread_create(&thread, NULL, thread_function, NULL);
// 回收線程并獲取返回值 pthread_join(thread, (void**)&thread_result);
printf("Thread returned: %dn", *thread_result); return 0;}
3.1、pthread_join() 的使用場景與注意事項
pthread_join() 是 阻塞函數,它會一直等待指定線程結束。如果目標線程需要執行大量計算或處理,調用 pthread_join() 的線程將一直處于等待狀態,直到目標線程終止。
如果線程已經結束,pthread_join() 將立即返回。
以下示例中,主線程在調用 pthread_join() 時會等待 5 秒,直到 worker_function() 執行完畢為止。
void* worker_function(void* arg) { sleep(5); // 模擬一些長時間運行的操作 return NULL;}
int main() { pthread_t thread;
pthread_create(&thread, NULL, worker_function, NULL);
printf("Waiting for thread to finish...n"); pthread_join(thread, NULL); // 阻塞等待線程結束
printf("Thread finished.n"); return 0;}
在進程中,如果父進程不回收子進程,則子進程會變為 僵尸進程,占用系統資源。
同樣的,如果一個線程終止后,沒有被其他線程調用 pthread_join() 來回收,其內存和其他資源也不會被立即釋放,這就導致了 僵尸線程 的問題。
僵尸線程不僅浪費資源,而且如果僵尸線程累積過多,可能會導致應用程序無法創建新的線程。
3.2、pthread_join() 與進程回收的區別
雖然 pthread_join() 與進程中的 waitpid() 類似,都是用于等待子線程(或子進程)結束并獲取其退出狀態,但二者之間有一些顯著的區別:
1、線程關系是對等的。
在多線程程序中,任何線程都可以調用 pthread_join() 來等待另一個線程的結束。即使是非創建該線程的線程,也可以調用 pthread_join() 來等待它的終止。線程之間沒有父子層級關系。
舉例來說,如果線程 A 創建了線程 B,線程 B 創建了線程 C,那么線程 A 可以等待線程 C 的結束,而不需要依賴線程 B。
這與進程的父子層級結構不同,父進程是唯一可以調用 wait() 或 waitpid() 來等待子進程終止的進程。
2、pthread_join() 不支持非阻塞等待。
pthread_join() 是阻塞調用,不支持類似waitpid() 中的非阻塞模式(通過傳入 WNOHANG 標志實現)。
這意味著線程調用 pthread_join() 后必須等待目標線程終止,不能做其他操作。如果需要更復雜的線程同步,通常需要引入其他機制,如 信號量、條件變量 等。
4
取消線程
通常情況下,線程會自行決定何時結束,比如通過調用 pthread_exit() 函數或者在其啟動函數中執行 return 語句。
但有些場景下,主線程或其他線程可能需要 強制終止 某個正在運行的線程,這時就可以通過 取消請求 來實現。
通過調用 pthread_cancel(),可以向目標線程發送一個取消請求,要求它終止。
pthread_cancel() 函數原型如下:
int pthread_cancel(pthread_t thread);
參數說明:
thread: 需要取消的目標線程的線程 ID。
返回值:
成功時返回 0。
如果調用失敗,返回錯誤碼,例如:ESRCH: 指定的線程不存在。
4.1、線程取消的響應機制
目標線程對取消請求的響應方式可以由其自身決定。每個線程都有一個 取消狀態 和 取消類型 來控制它對取消請求的響應:
4.1.1、取消狀態
取消狀態決定了線程是否允許響應取消請求,線程可以通過調用 pthread_setcancelstate() 來修改其取消狀態。
PTHREAD_CANCEL_ENABLE: 表示線程 允許 響應取消請求(這是默認狀態)。
PTHREAD_CANCEL_DISABLE: 表示線程 不允許 響應取消請求。即使收到了取消請求,線程仍會繼續運行,直到其取消狀態被重新設置為可取消。
pthread_setcancelstate() 函數原型:
int pthread_setcancelstate(int state, int *oldstate);
參數:
state: 可以是 PTHREAD_CANCEL_ENABLE 或 PTHREAD_CANCEL_DISABLE,分別表示開啟或禁用取消請求的響應。
oldstate: 如果不為 NULL,將保存原先的取消狀態。
示例如下:
pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, NULL); // 禁止取消請求pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, NULL); // 允許取消請求
4.1.2、取消類型
取消類型決定了線程 何時 響應取消請求。
可以通過調用 pthread_setcanceltype() 來設置線程的取消類型:
PTHREAD_CANCEL_DEFERRED: 線程將在 某些特定的取消點 響應取消請求(例如調用 pthread_testcancel(),或進行 I/O 操作時)。這是默認的取消類型。
PTHREAD_CANCEL_ASYNCHRONOUS: 線程在 收到取消請求的瞬間 就立即響應,可能導致線程在任意位置被取消。
pthread_setcanceltype() 函數原型:
int pthread_setcanceltype(int type, int *oldtype);
參數:
type: 可以是 PTHREAD_CANCEL_DEFERRED 或 PTHREAD_CANCEL_ASYNCHRONOUS,分別表示延遲響應取消或立即響應取消。
oldtype: 如果不為 NULL,將保存原先的取消類型。
示例:
pthread_setcanceltype(PTHREAD_CANCEL_ASYNCHRONOUS, NULL); // 設置為立即響應取消
pthread_setcanceltype(PTHREAD_CANCEL_DEFERRED, NULL); // 設置為延遲響應取消
4.2、取消點與線程清理
當線程的取消類型設置為 PTHREAD_CANCEL_DEFERRED 時,線程只有在到達某些 取消點 時才會響應取消請求。
取消點通常是一些耗時操作或系統調用,比如:
pthread_testcancel(): 顯式設置取消點。
其他一些常見的系統調用,比如 I/O 操作、sleep()、select() 等,都是隱式取消點。
系統中還有許多函數可以作為取消點,這里不再逐一列舉。您可以通過查看 man 手冊獲取更多信息,使用命令 man 7 pthreads 進行查詢。
pthread_testcancel() 函數原型:
void pthread_testcancel(void);
這個函數可以在代碼的任意位置顯式創建一個取消點。
調用 pthread_testcancel() 后,線程會檢查是否有取消請求,如果有,線程將在此處退出。
示例如下:
while (1) { // 執行一些任務 pthread_testcancel(); // 在循環中顯式設置取消點,檢查是否有取消請求}
4.3、線程清理處理函數
在線程終止時(無論是正常結束還是被取消),可以使用 清理處理函數 來進行資源清理。
清理處理函數可以確保線程在取消時能夠正確釋放資源,避免資源泄露。
使用 pthread_cleanup_push() 和 pthread_cleanup_pop() 來設置清理函數:
pthread_cleanup_push(void (*routine)(void *), void *arg):將一個清理函數 routine 壓入棧,當線程退出時,系統將調用該函數。
pthread_cleanup_pop(int execute):將清理函數從棧中彈出,execute 表示是否執行該函數。
以下例子中,當線程收到取消請求后,它會在 pthread_testcancel() 函數處響應取消請求并退出。
在線程退出時,cleanup_handler() 會被調用以清理資源。
void cleanup_handler(void *arg) { printf("Cleanup: %sn", (char *)arg);}
void* thread_function(void* arg) { pthread_cleanup_push(cleanup_handler, "Thread resources"); // 設置清理函數 while (1) { printf("Thread running...n"); sleep(1); pthread_testcancel(); // 檢查是否有取消請求 } pthread_cleanup_pop(1); // 1 表示執行清理函數 return NULL;}
int main() { pthread_t thread; pthread_create(&thread, NULL, thread_function, NULL);
sleep(3); // 等待幾秒鐘 pthread_cancel(thread); // 發送取消請求 pthread_join(thread, NULL); // 等待線程結束 printf("Thread has been canceled.n");
return 0;}
正確處理線程的取消操作對于復雜的多線程應用程序至關重要,特別是在執行長時間任務時,靈活管理線程的取消狀態和清理行為能夠有效提高系統的穩定性和可靠性。
pthread_cancel() 用于向目標線程發送取消請求,要求其終止,但目標線程是否終止取決于其取消狀態和取消類型。
線程可以通過 pthread_setcancelstate() 來控制是否響應取消請求,并通過 pthread_setcanceltype() 來控制何時響應。
在使用 延遲取消 的情況下,線程只有在特定的 取消點 處才會檢查取消請求,可以通過 pthread_testcancel() 顯式設置取消點。
清理處理函數 確保線程在被取消時能夠正確釋放資源,避免資源泄露。
5
分離線程
默認情況下,線程終止后,其資源不會立即被系統回收,除非有另一個線程通過 pthread_join() 函數顯式地等待該線程終止,回收其資源。
但如果某些線程的退出狀態和返回值對程序來說并不重要,且不希望手動調用 pthread_join(),可以將該線程設置為 分離狀態。
分離狀態的線程在終止時,系統會自動回收它的資源。
要將線程設置為分離狀態,可以調用 pthread_detach() 函數。
pthread_detach() 函數原型:
int pthread_detach(pthread_t thread);
參數說明:
thread: 需要分離的目標線程的線程 ID。
返回值:
成功時返回 0。
如果調用失敗,返回錯誤碼,例如:
ESRCH: 指定的線程不存在或已經被回收。
EINVAL: 線程已經處于分離狀態。
調用 pthread_detach() 后,指定的線程會進入分離狀態。
處于分離狀態的線程在終止時,系統會自動回收其所有資源,而無需其他線程顯式調用 pthread_join()。
分離狀態是不可逆的,一旦線程被分離,就不能再通過 pthread_join() 獲取該線程的返回狀態或等待其結束。
以下例子中,創建了一個新線程,并通過 pthread_detach() 將其分離。之后,無需調用 pthread_join(),系統將在該線程終止時自動回收它的資源。
pthread_t thread;pthread_create(&thread, NULL, thread_function, NULL);pthread_detach(thread); // 將該線程設置為分離狀態
線程不僅可以由其他線程分離,還可以通過調用 pthread_detach(pthread_self()) 來 分離自己。
這意味著該線程在終止時不需要其他線程來回收資源,系統將自動處理。
示例如下:
void* thread_function(void* arg) { pthread_detach(pthread_self()); // 分離自己 // 線程執行的其他操作 pthread_exit(NULL);}
線程分離機制特別適用于以下幾種場景:
不關心線程的返回值: 如果線程執行的任務不需要返回值,且不希望其他線程顯式地等待它結束。
避免僵尸線程: 僵尸線程是指已經終止但資源未被回收的線程,長時間存在僵尸線程會消耗系統資源。將線程設置為分離狀態,可以避免僵尸線程的產生。
長時間運行的后臺任務: 如果線程運行時間較長或是后臺任務,而主線程不需要等待其結束,分離該線程可以簡化資源管理。
線程分離與 pthread_join() 的比較:
線程分離:
使用 pthread_detach() 將線程設置為分離狀態。
系統在線程終止時自動回收資源。
無法通過 pthread_join() 獲取線程的返回值或等待線程終止。
pthread_join():
主動調用 pthread_join() 等待指定線程終止并回收資源。
可以獲取線程的返回值或終止狀態。
若沒有調用 pthread_join(),線程終止后會成為僵尸線程,直到其資源被回收。
線程分離在簡化多線程程序的資源管理方面非常有用,特別是對于一些無需等待或回收的線程,可以通過分離機制優化程序的性能和穩定性。
最后講兩點注意事項:
不可逆性: 一旦線程被設置為分離狀態,就無法恢復到可被 pthread_join() 回收的狀態。如果你將某個線程分離,后續便無法獲取其返回值或等待它結束。
線程同步問題: 如果某個線程執行的任務需要與其他線程同步完成,則不應將其分離。否則,主線程或其他線程可能無法等待該線程結束,導致任務未完成就繼續執行。
*博客內容為網友個人發布,僅代表博主個人觀點,如有侵權請聯系工作人員刪除。