1. 程式人生 > >終止正在執行的子執行緒(一、幾種方式的介紹)

終止正在執行的子執行緒(一、幾種方式的介紹)

最近開發的東西有涉及到執行緒的建立和釋放,由於對這一塊不是很熟悉,查閱很多資料,現記錄如下:

如何正確的終止正在執行的子執行緒

      最近開發一些東西,執行緒數非常之多,當用戶輸入Ctrl+C的情形下,預設的訊號處理會把程式退出,這時有可能會有很多執行緒的資源沒有得到很好的釋放,造成了記憶體洩露等等諸如此類的問題,所以,就如上述使用場景一般,如何從待外部安全的終止正在執行的執行緒,就是本文要討論的內容。

    首先我們來看一下,讓當前正在執行的子執行緒停止的所有方法

1.任何一個執行緒呼叫exit

2.pthread_exit

3.pthread_kill

4.pthread_cancel

PS:下面的程式碼都會用到一個函式checkResults:

static void checkResults(char *string, int rc) {
  if (rc) {
    printf("Error on : %s, rc=%d",
           string, rc);
    exit(EXIT_FAILURE);
  }
  return;
}

 下面進行一一分析:

1)任何一個執行緒呼叫exit

      任何一個執行緒只要呼叫了exit都會導致程序結束,各種子執行緒當然也能很好的結束了,可是這種退出會有一個資源釋放的問題.我們知道當一個程序終止時,核心對該程序所有尚未關閉的檔案描述符呼叫

close關閉,所以即使使用者程式不呼叫close,在終止時核心也會自動關閉它開啟的所有檔案。標準C++ IO流也會很好的在exit退出時得到flush並且釋放資源,這些東西並不會造成資源的浪費(系統呼叫main函式入口類似於exit(main(argc,argv))).實際上,無論程序怎樣結束,系統都將會釋放掉所有程式碼所申請的資源,無論是堆上的還是棧上的。這種結束所有執行緒(包括主執行緒)的方式實際上在很多時候是非常可取的,但是對於針對關閉時進行一些別的邏輯的處理(指非資源釋放邏輯)就不會很好,例如我想在程式被kill掉之前統計一下完成了多少的工作,這個統計類似於MapReduce,需要去每個執行緒獲取,並且最後歸併成一個統一的結果等等場景。

2) pthread_exit

    當前執行的執行緒執行到pthread_exit後直接退出執行緒。對於各個子執行緒能夠清楚地知道自己在什麼時候結束的情景下,這個函式非常好用。可是實際上很多時候一個執行緒不知道自己在什麼時候會結束,例如遭遇Ctrl+C時,kill程序時。當然如果排除所有的外界干擾的話,那就讓每個執行緒幹完自己的事情後,然後自覺地乖乖的呼叫pthread_exit就可以了。

這裡還有一種方法,既然子執行緒可以通過pthread_exit來正確退出,那麼我們可以在遭遇Ctrl+C時,kill程序時處理signal訊號,然後分別給在某一個執行緒可以訪問的公共區域存上一個flag變數,執行緒內部每執行一段時間(很短)來檢查一下flag,若發現需要終止自己時,自己呼叫pthread_exit,此法有一個弱點就是當子執行緒需要進行阻塞的操作時,可能無暇顧及檢查flag,例如socket阻塞操作。如果你的子執行緒的任務基本沒有非阻塞的函式,那麼這麼做也不失為一種很好的方案。

3) pthread_kill

   不要被這個可怕的邪惡的名字所嚇倒,其實pthread_kill並不像他的名字那樣威力大,使用之後,你會感覺,他徒有虛名而已。

    pthread_kill的職責其實只是向指定的執行緒傳送signal訊號而已,並沒有真正的kill掉一個執行緒,當然這裡需要說明一下,有些訊號的預設行為就是exit,那此時你使用pthread_kill傳送訊號給目標執行緒,目標執行緒會根據這個訊號的預設行為進行操作,有可能是exit。當然我們同時也可以更改獲取某個訊號的行為,以此來達到我們終止子執行緒的目的。

#define _MULTI_THREADED
#include <pthread.h>
#include <stdio.h>
#include <signal.h>
#include "check.h"

/*
struct sigaction {
         __sighandler_t sa_handler;
        unsigned long sa_flags;
        void (*sa_restorer)(void);
        sigset_t sa_mask;   /* mask last for extensibility */
};
*/

void sighand(int signo);

void *threadfunc(void *parm)
{
  pthread_t             self = pthread_self();
  pthread_id_np_t       tid;
  int                   rc;

  pthread_getunique_np(&self, &tid);
  printf("Thread 0x%.8x %.8x entered\n", tid);
  errno = 0;
  rc = sleep(30);
  if (rc != 0 && errno == EINTR) {
    printf("Thread 0x%.8x %.8x got a signal delivered to it\n",
           tid);
    return NULL;
  }
  printf("Thread 0x%.8x %.8x did not get expected results! rc=%d, errno=%d\n",
         tid, rc, errno);
  return NULL;
}

int main(int argc, char **argv)
{
  int                     rc;
  int                     i;
  struct sigaction        actions;
  pthread_t               threads;

  printf("Enter Testcase - %s\n", argv[0]);
 
  printf("Set up the alarm handler for the process\n");
  memset(&actions, 0, sizeof(actions));
  sigemptyset(&actions.sa_mask);
  actions.sa_flags = 0;
  actions.sa_handler = sighand;

  rc = sigaction(SIGALRM,&actions,NULL);
  checkResults("sigaction\n", rc);

  rc = pthread_create(&threads, NULL, threadfunc, NULL);
  checkResults("pthread_create()\n", rc);

  sleep(3);

  rc = pthread_kill(threads, SIGALRM);
  checkResults("pthread_kill()\n", rc);

  rc = pthread_join(threads, NULL);
  checkResults("pthread_join()\n", rc);

  printf("Main completed\n");
  return 0;
}

void sighand(int signo)
{
  pthread_t             self = pthread_self();
  pthread_id_np_t       tid;
 
  pthread_getunique_np(&self, &tid);
  printf("Thread 0x%.8x %.8x in signal handler\n",
         tid);
  return;
}

執行結果:

Output:

Enter Testcase - QP0WTEST/TPKILL0
Set up the alarm handler for the process
Thread 0x00000000 0000000c entered
Thread 0x00000000 0000000c in signal handler
Thread 0x00000000 0000000c got a signal delivered to it
Main completed
  可見,我們可以通過截獲的signal訊號,來釋放掉執行緒申請的資源,可是遺憾的是我們不能在signal處理函式內呼叫pthread_exit來終結掉我們想要終結的執行緒,因為pthread_exit只能終結當前執行緒,而signal被呼叫的方式可以理解為核心的回撥,不是在同一個執行緒執行的,所以這裡只能做處理釋放資源的事情,而執行緒內部只能通過判斷有沒有被中斷(一般是EINTR)來決定是否要求自己結束,判定後可以呼叫pthread_exit退出。

  此法對於一般的操作也是非常可行的,可是在有的情況下就不是一個比較好的方法了,比如我們有一些執行緒在處理網路IO事件,假設它是一種一個客戶端對應一個伺服器執行緒,阻塞從Socket中讀訊息的情況。我們一般在網路IO的庫裡面會加上對EINTR訊號的處理,例如recv時發現返回值小於0,檢查error後,會進行他對應的操作。有可能它會再recv一次,那就相當於我的執行緒根本就不會終止,因為網路IO的類有可能不知道在獲取EINTR時要終止執行緒。也就是說這不是一個特別好的可移植方案,如果你執行緒裡的操作使用了很多外來的不太熟悉的類,而且你並不知道它對EINTR的處理手段是什麼,這時你在使用這樣的方法來終止就有可能出問題了。而且如果你不是特別熟悉這方面的話你會很苦惱,為什麼我的測試程式碼全是ok的,一加入你們部門開發的框架進來就不ok了,肯定是你們框架出問題了。好了,為了不必要的麻煩,我最後沒有使用這個方案。

3)pthread_cancel

    這個方案是我最終採用的方案,我認為是解決這個問題,相比較而言,通用的最好的解決方案。

   pthread_cancel可以單獨使用,因為在很多系統函式裡面本身就有很多的斷點,當呼叫這些系統函式時就會命中其內部的斷點來結束執行緒如下面的程式碼中,即便註釋掉我們自己設定的斷點pthread_testcancel()程式還是一樣的會被成功的cancel掉,因為printf函式內部有取消點(如果想了解更多的函式的取消點情況,可以閱讀《Unix高階環境程式設計》的執行緒部分

#include <pthread.h>
#include <stdio.h>
#include<stdlib.h>
#include <unistd.h>
void *threadfunc(void *parm)
{
  printf("Entered secondary thread\n");
  while (1) {
    printf("Secondary thread is looping\n");
    pthread_testcancel();
    sleep(1);
  }
  return NULL;
}

int main(int argc, char **argv)
{
  pthread_t             thread;
  int                   rc=0;

  printf("Entering testcase\n");

  /* Create a thread using default attributes */
  printf("Create thread using the NULL attributes\n");
  rc = pthread_create(&thread, NULL, threadfunc, NULL);
  checkResults("pthread_create(NULL)\n", rc);

  /* sleep() is not a very robust way to wait for the thread */
  sleep(1);

  printf("Cancel the thread\n");
  rc = pthread_cancel(thread);
  checkResults("pthread_cancel()\n", rc);

  /* sleep() is not a very robust way to wait for the thread */
  sleep(10);
  printf("Main completed\n");
  return 0;
}

輸出:

Entering testcase
Create thread using the NULL attributes
Entered secondary thread
Secondary thread is looping
Cancel the thread
Main completed
  POSIX保證了絕大部分的系統呼叫函式內部有取消點,我們看到很多在cancel呼叫的情景下,recvsend函式最後都會設定pthread_testcancel()取消點,其實這不是那麼有必要的。那麼究竟什麼時候該pthread_testcancel()出場呢?在《Unix高階環境程式設計》中有說,當遇到大量的基礎計算時(如科學計算),需要自己來設定取消點

   ok,得益於pthread_cancel,我們很輕鬆的把執行緒可以cancel掉,可是我們的資源呢?何時釋放...

       下面來看兩個pthread函式

void pthread_cleanup_push(void (*routine)(void *), void *arg);  
void pthread_cleanup_pop(int execute); 
這兩個函式能夠保證在函式pthread_cleanup_push呼叫之後,函式pthread_cleanup_pop呼叫之前,任何形式的執行緒結束後都會呼叫之前向pthread_cleanup_push註冊的回撥函式

        另外我們還可通過下面這個函式來設定一些狀態

int pthread_setcanceltype(int type, int *oldtype); 

Cancelability

Cancelability State

Cancelability Type

disabled

PTHREAD_CANCEL_DISABLE

PTHREAD_CANCEL_DEFERRED

disabled

PTHREAD_CANCEL_DISABLE

PTHREAD_CANCEL_ASYNCHRONOUS

deferred

PTHREAD_CANCEL_DISABLE

PTHREAD_CANCEL_DEFERRED

asynchronous

PTHREAD_CANCEL_ENABLE

PTHREAD_CANCEL_ASYNCHRONOUS

    當設定typePTHREAD_CANCEL_ASYNCHRONOUS時,執行緒並不會等待命中取消點才結束,而是立馬結束。

#include <pthread.h>                                                            
#include <stdio.h>                                                              
#include <stdlib.h>                                                             
#include <unistd.h>                                                             
#include <errno.h>                                                                                
int footprint=0;
char *storage; 

void freerc(void *s)
{
   free(s);
   puts("the free called");    
}                                                               
                                                                               
void *thread(void *arg) {                                                                                                                    
  int           rc=0, oldState=0; 
  rc = pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &oldState);   //close the cancel switch   
  checkResults("pthread_setcancelstate()\n", rc);                                                                        
  if ((storage = (char*) malloc(80)) == NULL) {                                 
    perror("malloc() failed");                                                  
    exit(6);                                                                    
  }                                                                             
   rc = pthread_setcancelstate(PTHREAD_CANCEL_ENABLE,&oldState);   //open the cancel  switch                                                                        
   checkResults("pthread_setcancelstate(2)\n", rc);  
/* Plan to release storage even if thread doesn't exit normally */            
                                                                                
  pthread_cleanup_push(freerc, storage);   /*the freerc is method here, you can use your own method*/                                       
                                                                                
  puts("thread has obtained storage and is waiting to be cancelled");           
  footprint++;                                                                  
  while (1)
  {      
     pthread_testcancel();   //make a break point here     
     //pthread_exit(NULL);    //test exit to exam whether the freerc method called                                                     
     sleep(1);  
  }                                                                 
                                                                                
  pthread_cleanup_pop(1);                                                       
}                                                                               
                                                                                
main() {                                                                        
  pthread_t thid; 
  void      *status=NULL;                                                              
                                                                                
  if (pthread_create(&thid, NULL, thread, NULL) != 0) {                         
    perror("pthread_create() error");                                           
    exit(1);                                                                    
  }                                                                             
                                                                                
  while (footprint == 0)                                                        
    sleep(1);                                                                   
                                                                                
  puts("IPT is cancelling thread");                                             
                                                                                
  if (pthread_cancel(thid) != 0) {                                              
    perror("pthread_cancel() error");   
    sleep(2);                                        
    exit(3);                                                                    
  }                                                                             
                                                                                
  if (pthread_join(thid, &status) != 0) {  
    if(status != PTHREAD_CANCELED){                                      
        perror("pthread_join() error");                                             
        exit(4);
    }                                                                    
  }
 if(status == PTHREAD_CANCELED)
    puts("PTHREAD_CANCELED");
     
 puts("main exit");                                                                        
}