1. 程式人生 > >使用pthread_cancel終止執行緒的填坑歷程

使用pthread_cancel終止執行緒的填坑歷程

開頭說明一句:使用pthread_cancel是一個喪心病狂的想法。

首先是常識

pthread_cancel(thread)會發送終止訊號給thread執行緒,如果成功則返回0,否則為非0值。傳送成功並不意味著thread會終止。
若是在整個程式退出時,要終止各個執行緒,應該在成功傳送 CANCEL 指令後,使用 pthread_join 函式,等待指定的執行緒已經完全退出以後,再繼續執行;否則,很容易產生 “段錯誤”。

然後是進一步的認識

設定本執行緒對Cancel訊號的反應
int pthread_setcancelstate(int state, int *oldstate
); state有兩種值:PTHREAD_CANCEL_ENABLE(預設)和PTHREAD_CANCEL_DISABLE 分別表示收到訊號後設為CANCLED狀態和忽略CANCEL訊號繼續執行;old_state如果不為NULL則存入原來的Cancel狀態以便恢復。 設定本執行緒取消動作的執行時機 int pthread_setcanceltype(int type, int *oldtype); type由兩種取值:PTHREAD_CANCEL_DEFFERED和PTHREAD_CANCEL_ASYCHRONOUS 僅當Cancel狀態為Enable時有效,分別表示收到訊號後繼續執行至下一個取消點再退出和立即執行取消動作(退出);oldtype如果不為NULL則存入運來的取消動作型別值。 手動建立一個取消點 void pthread_testcancel(void) 檢查本執行緒是否處於Canceld狀態,如果是,則進行取消動作,否則直接返回。 此函式線上程內執行,執行的位置就是執行緒退出的位置,在執行此函式以前,執行緒內部的相關資源申請一定要釋放掉,他很容易造成記憶體洩露。

那麼,問題來了…

/*
這是一段虛擬碼,看懂意思就成
main裡面不斷判斷執行緒,如果test_thd存在就刪除,如果不存在,就建立.
這裡面建立執行緒的屬性統一為預設狀態(PTHREAD_CANCEL_ENABLE).
*/

static sem_t mutex;
static int iswork = 0;  //用來判斷執行緒狀態
static void *test_thread_handler();
static void *select_read();

int main( int argc, char *argv[] )
{
    pthread_t test_thd;
    iswork = 0
; sem_init(&mutex, 0, 1 ); //初始化鎖 while(1) { get_task(); //阻塞等待任務到來(訊息佇列阻塞) if(iswork) { pthread_cancel(test_thd); iswork = 0; } else { sem_wait(&mutex); //加鎖 (0,0)->(0,1)->(0,0) write(); //寫串列埠 sem_post(&mutex); //解鎖 (0,0)->(0,1) pthread_create(&test_thd,NULL, test_thread_handler,NULL); } } return 0; } static void *test_thread_handler() { iswork = 1; int time = 5; //5秒 pthread_detach(pthread_self()); //分離 int ret = socket_select(time);//一個5秒的阻塞,時間內有觸發立即返回1 printf("select over %d\t%d\n",ret,time); if(ret > 0) { select_read(); } printf("thread over\n"); iswork = 0; return 0; } static void select_read() { sem_wait(&mutex); //加鎖 (0,0)->(0,1)->(0,0) read(); //讀串列埠,未確定是否為阻塞(取消點) sem_post(&mutex); //解鎖 (0,0)->(0,1) printf("read over\n"); }

可以看出來,pthread_cancel(test_thd)時會有4種情況:
1. test_thd已經執行結束,取消失敗

select over 0 0
read over
thread over

2.test_thd正阻塞在socket_select,假設已經阻塞了2秒

select over 0 3
thread over
這裡至今搞不太明白第二種情況為什麼明明已經cancel了執行緒,但程式還是會執行下去,列印 thread over

3.test_thd正阻塞在read,假設socket_select在1秒內就收到了訊號

select over 1 4
一直阻塞

4.test_thd正執行在sem_wait(&mutex);之前
不列印

很明顯,第三種情況就使執行緒陷入了死鎖,但還沒搞懂程式是阻塞在了main裡面的sem_wait(),還是阻塞在了test裡面的sem_post()

填坑歷程,學到不少

先入為主認為訊息佇列阻塞

很不幸在發現bug的時候對執行緒的cancel僅僅只有普通的常識和簡單的認識。
同時由於專案的read時間的確是極短,使得bug很難重現,於是第一時間認為阻塞的根源在於get訊息佇列的時候。

static void PrintSysInfo(struct msginfo *stInfo)
{
    printf("-------------- MSG_INFO --------------\n");
    printf("msg queue num       : %d\n",stInfo->msgpool);
    printf("msg total num       : %d\n",stInfo->msgmap);
    printf("msg total len       : %d\n",stInfo->msgtql);

    printf("single msg max len       : %d\n",stInfo->msgmax);
    printf("all msg max len       : %d\n",stInfo->msgmnb);
    printf("Max msg num       : %d\n",stInfo->msgmni);
    printf("--------------------------------------\n");

    return;
}

    msgctl(qid_ipc, MSG_INFO, &stMsq);
    PrintSysInfo((struct msginfo*)&stMsq);//列印系統當前佇列資訊
    msgctl(qid_ipc, IPC_STAT, &stMsq);
    printf("%u",(int)stMsq.msg_qnum);//列印當前佇列訊息條數

也是巧了,發現裝置的linux核心裡面的訊息佇列最大空間僅有16k,而我每條訊息有的甚至超過2k 來個八九條就成撐滿,一般來說也不會同時阻塞了八九條訊息這麼多,然後我深以為此乃問題所在。遂修改資料結構,把每條訊息大小縮減至150位元組。雖然最後沒有解決根源問題,但也解決了這麼一個隱藏bug。

老大提示可能是編譯器優化導致

優化了訊息佇列的資料結構之後,發現問題還是沒有得到解決,此時還沒想到是程式陷入了死鎖的原因,單純認為只是阻塞了,於是把點放到iswork這個變數上。
路過的老大給了我一個C語言的關鍵字:volatile
原本就孤陋寡聞的我瞬間茅廁頓開,差點就高潮了,有一種認為自己還是圖樣圖森破的覺悟,後來發現自己果然還是圖樣圖森破。
附volatile關鍵字的用法和使用情景。

volatile是一個型別修飾符(type specifier)。它是被設計用來修飾被不同執行緒訪問和修改的變數。如果不加入volatile,基本上會導致這樣的結果:要麼無法編寫多執行緒程式,要麼編譯器失去大量優化的機會。

某些編譯器會對變數做一些優化,當編譯器發現連續兩次讀一個數據的時候沒有在程式碼裡面對這個變數進行過操作,就會把這個資料的值拷到一個臨時的資料中,下次再讀這個資料的時候會直接返回臨時的資料,而不是去讀這個資料本身。

在本次執行緒內,當讀取一個變數時,為提高存取速度,編譯器優化時有時會先把變數讀取到一個暫存器中;以後再取變數值時,就直接從暫存器中取值;
當變數值在本執行緒裡改變時,會同時把變數的新值copy到該暫存器中,以便保持一致

當變數在因別的執行緒等而改變了值,該暫存器的值不會相應改變,從而造成應用程式讀取的值和實際的變數值不一致。
當該暫存器在因別的執行緒等而改變了值,原變數的值不會改變,從而造成應用程式讀取的值和實際的變數值不一致。

一個定義為volatile的變數是說這變數可能會被意想不到地改變,這樣,編譯器就不會去假設這個變數的值了。精確地說就是,優化器在用到這個變數時必須每次都小心地重新讀取這個變數的值,而不是使用儲存在暫存器裡的備份。

此致向我老大敬禮。

加入大量列印,sleep延遲除錯,終於定位出問題根源

由於read的時間實在太短,bug的重現使debug遇到很大的阻礙,再推翻很多設想後,重新加入sleep一點一點得測,終於皇天不負有心人,發現程式是陷入死鎖了。這本來是個很容易就能想到的問題,但是我剛想到的時候我就直接進在每次讀之前手動解鎖,但是依然沒有效果,我就略過了這個罪魁惡首。

        if(iswork)
        {
            //如果正在讀,等它讀完再鎖,在取消執行緒,再解鎖
            sem_wait(&mutex);  //加鎖  (0,0)->(0,1)->(0,0)
            pthread_cancel(test_thd);
            sem_post(&mutex);  //解鎖  (0,0)->(0,1)
            iswork = 0;
        }
        else
        {
            sem_wait(&mutex);  //加鎖  (0,0)->(0,1)->(0,0)
            write();  //寫串列埠
            sem_post(&mutex);  //解鎖  (0,0)->(0,1)

            pthread_create(&test_thd,NULL, test_thread_handler,NULL);
        }

至此,問題得到解決,歷時三天。
需求功能是本地從訊息佇列獲取任務,通過TTL串列埠傳送任務到目標裝置,目標裝置會先後返回兩個訊號:1.收到後回覆B碼,2.處理後回覆N碼。
本機收到B碼後要去訊息佇列等待下一個任務,同時等待目標裝置N碼。如果在等待N碼的過程中有緊急任務要處理,會直接傳送新的資料到目標裝置。

本問題的根源終其原因應該是架構問題。
最理想的解決方案當然應該是起兩條執行緒,一條專門去讀,一條專門去寫,長時間存在,互不干擾。

但是在做的過程中有太多其他細節和各種因素,最終只能使用在一條執行緒中不斷建立刪除執行緒去實現。其中更改需求的辛酸過來人都懂吧。

今晚想吃爆炒羊駝。