1. 程式人生 > 程式設計 >簡單瞭解C語言中主執行緒退出對子執行緒的影響

簡單瞭解C語言中主執行緒退出對子執行緒的影響

這篇文章主要介紹了簡單瞭解C語言中主執行緒退出對子執行緒的影響,文中通過示例程式碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友可以參考下

對於程式來說,如果主程序在子程序還未結束時就已經退出,那麼Linux核心會將子程序的父程序ID改為1(也就是init程序),當子程序結束後會由init程序來回收該子程序。

那如果是把程序換成執行緒的話,會怎麼樣呢?假設主執行緒在子執行緒結束前就已經退出,子執行緒會發生什麼?

在一些論壇上看到許多人說子執行緒也會跟著退出,其實這是錯誤的,原因在於他們混淆了執行緒退出和程序退出概念。實際的答案是主執行緒退出後子執行緒的狀態依賴於它所在的程序,如果程序沒有退出的話子執行緒依然正常運轉。如果程序退出了,那麼它所有的執行緒都會退出,所以子執行緒也就退出了。

主執行緒先退出

先來看一個主執行緒先退出的例子:

#include <pthread.h>
#include <unistd.h>

#include <stdio.h>

void* func(void* arg)
{
  pthread_t main_tid = *static_cast<pthread_t*>(arg);
  pthread_cancel(main_tid);
  while (true)
  {
    //printf("child loops\n");
  }
  return NULL;
}

int main(int argc,char* argv[])
{
  pthread_t main_tid = pthread_self();
  pthread_t tid = 0;
  pthread_create(&tid,NULL,func,&main_tid);
  while (true)
  {
    printf("main loops\n");
  }
  sleep(1);
  printf("main exit\n");
  return 0;
}

把主執行緒的執行緒號傳給子執行緒,在子執行緒中通過pthread_cancel終止主執行緒使其退出。執行程式,可以發現在列印了一定數量的「main loops」之後程式就掛起了,但卻沒有退出。

主執行緒因為被子執行緒終止了,所有沒有看到「main exit」的列印。子執行緒終止了主執行緒後進入了死迴圈while中,所以程式看起來像掛起了。如果我們讓子程序while迴圈中的列印語句生效再執行就可以發現程式會一直列印「child loops」字樣。

主執行緒被子執行緒終止了,但他們所依賴的程序並沒有退出,所以子執行緒依然正常運轉。

主執行緒隨程序一起退出

之前看到一些人說如果主執行緒先退出了,子執行緒也會跟著退出,其實他們混淆了執行緒退出和程序退出的概念。下面這個例子代表了他們的觀點:

void* func(void* arg)
{
  while (true)
  {
    printf("child loops\n");
  }
  return NULL;
}

int main(int argc,&main_tid);
  sleep(1);
  printf("main exit\n");
  return 0;
}

執行上面的程式碼,會發現程式在列印一定數量的「child loops」和一句「main exit」之後退出,並且在退出之前的最後一句列印是「main exit」。

按照他們的邏輯,你看,因為主執行緒在列印完「main exit」後退出了,然後子執行緒也跟著退出了,所以隨後就沒有子執行緒的列印了。

但其實這裡是混淆了程序退出和執行緒退出的概念了。實際的情況是主執行緒中的main函式執行完ruturn後彈棧,然後呼叫glibc庫函式exit,exit進行相關清理工作後呼叫_exit系統呼叫退出該程序。所以,這種情況實際上是因為程序執行完畢退出導致所有的執行緒也都跟著退出了,並非是因為主執行緒的退出導致子執行緒也退出。

Linux執行緒模型

實際上,posix執行緒和一般的程序不同,在概念上沒有主執行緒和子執行緒之分(雖然在實際實現上還是有一些區分),如果仔細觀察apue或者unp等書會發現基本看不到「主執行緒」或者「子執行緒」等詞語,在csapp中甚至都是用「對等執行緒」一詞來描述執行緒間的關係。

在Linux 2.6以後的posix執行緒都是由使用者態的pthread庫來實現的。在使用pthread庫以後,在使用者視角看來,每一個tast_struct就對應一個執行緒(tast_struct原本是核心對應一個程序的結構),而一組執行緒以及他們所共同引用的一組資源就是程序。從Linux 2.6開始,核心有了執行緒組的概念,tast_struct結構中增加了一個tgid(thread group id)欄位。getpid(獲取程序號)通過系統呼叫返回的也是tast_struct中的tgid,所以tgid其實就是程序號。而tast_struct中的執行緒號pid欄位則由系統呼叫syscall(SYS_gettid)來獲取。

當執行緒收到一個kill致命訊號時,核心會將處理動作施加到整個執行緒組上。為了應付「傳送給程序的訊號」和「傳送給執行緒的訊號」,tast_struct裡面維護了兩套signal_pending,一套是執行緒組共用的,一套是執行緒獨有的。通過kill傳送的致命訊號被放線上程組共享的signal_pending中,可以任意由一個執行緒來處理。而通過pthread_kill傳送的訊號被放線上程獨有的signal_pending中,只能由本執行緒來處理。

關於執行緒與訊號,apue有這麼幾句:

每個執行緒都有自己的訊號遮蔽字,但是訊號的處理是程序中所有執行緒共享的。這意味著儘管單個執行緒可以阻止某些訊號,但當執行緒修改了與某個訊號相關的處理行為以後,所有的執行緒都必須共享這個處理行為的改變。這樣如果一個執行緒選擇忽略某個訊號,而其他的執行緒可以恢復訊號的預設處理行為,或者是為訊號設定一個新的處理程式,從而可以撤銷上述執行緒的訊號選擇。

如果訊號的預設處理動作是終止該程序,那麼把訊號傳遞給某個執行緒仍然會殺掉整個程序。

例如一個程式a.out建立了一個子執行緒,假設主執行緒的執行緒號為9601,子執行緒的執行緒號為9602(它們的tgid都是9601),因為預設沒有設定訊號處理程式,所以如果執行命令kill 9602的話,是可以把9601和9602這個兩個執行緒一起殺死的。如果不知道Linux執行緒背後的故事,可能就會覺得遇到靈異事件了。

另外系統呼叫syscall(SYS_gettid)獲取的執行緒號與pthread_self獲取的執行緒號是不同的,pthread_self獲取的執行緒號僅僅線上程所依賴的程序內部唯一,在pthread_self的man page中有這樣一段話:

Thread IDs are guaranteed to be unique only within a process. A thread ID may be reused after a terminated thread has been joined,or a detached thread has terminated.

所以在核心中唯一標識執行緒ID的執行緒號只能通過系統呼叫syscall(SYS_gettid)獲取。

以上就是本文的全部內容,希望對大家的學習有所幫助,也希望大家多多支援我們。