2008-10-15 75 views
23

我有一個線程在後臺運行,以阻塞方式從輸入設備讀取事件,現在當我退出應用程序時,我想正確清理線程,但是我不能只運行pthread_join(),因爲該線程永遠不會因阻塞IO而退出。如何加入掛在阻塞IO上的線程?

我該如何正確地解決這種情況?我應該發送一個pthread_kill(theard,SIGIO)還是一個pthread_kill(theard,SIGALRM)來打破這個塊?這是否是正確的信號?或者有另一種方法來解決這種情況,並讓該子線程退出阻塞讀取?

目前有點困惑,因爲我的谷歌搜索沒有找到解決方案。

這是在Linux上並使用pthreads。編輯:我在SIGIO和SIGALRM中玩過一段時間,當我沒有安裝信號處理程序時,它們會阻止IO,但在控制檯上給出消息(「I/O possible」),但是當我安裝一個信號處理程序,爲了避免該消息,它們不再中斷阻塞IO,因此線程不會終止。所以我回到第一步。

+0

QQQ似乎有正確答案,不幸的是有極少數的選票。 `pthread_cancel`是解決您的問題的方法。 – 2010-09-27 03:30:19

+0

只要線程仍然被阻塞,它不會造成任何傷害。問題是如果線程在你關閉的時候醒來。所以解決的辦法是在阻塞線之後放置一些代碼,以阻止線程執行任何操作*其他*如果正在關閉進程。 – 2011-09-28 17:15:12

+0

類似的問題和可能的解決方案有討論:[文件描述符和多線程程序(http://www.ddj.com/hpc-high-performance-computing/212001285) – dmityugov 2008-11-10 12:58:48

回答

2

老問題,可以很好地得到一個新的答案,因爲事情已經演變,並且現在有一項新技術可用於更好處理線程中的信號。

由於Linux內核2.6。22,系統提供了一個稱爲signalfd()新功能,可以用來打開一組給定的Unix信號的文件描述符(那些徹底殺死一個過程之外。)

// defined a set of signals 
sigset_t set; 
sigemptyset(&set); 
sigaddset(&set, SIGUSR1); 
// ... you can add more than one ... 

// prevent the default signal behavior (very important) 
sigprocmask(SIG_BLOCK, &set, nullptr); 

// open a file descriptor using that set of Unix signal 
f_socket = signalfd(-1, &set, SFD_NONBLOCK | SFD_CLOEXEC); 

現在你可以使用poll()select()函數可以用來偵聽您正在偵聽的更常用的文件描述符(套接字,磁盤上的文件等)上的信號。如果你想有一個循環,可以檢查信號和其他文件描述符一遍又一遍

的NONBLOCK是重要的(即它也是你的其他文件描述符重要)。

我有使得與(1)的定時器的工作原理的實現中,(2)的插座,(3)管道,(4)的Unix信號,(5)常規文件。其實,真的是任何文件描述符加定時器。

https://github.com/m2osw/snapcpp/blob/master/snapwebsites/libsnapwebsites/src/snapwebsites/snap_communicator.cpp
https://github.com/m2osw/snapcpp/blob/master/snapwebsites/libsnapwebsites/src/snapwebsites/snap_communicator.h

您也可以通過圖書館感興趣如libevent

2

正如你所說,我認爲唯一的方法就是發送一個信號,然後適當地捕捉並處理它。替代品可能是SIGTERM,SIGUSR1,SIGQUIT,SIGHUP,SIGINT等。

您也可以在輸入描述符上使用select(),以便只在準備好時纔讀取。您可以使用select(),例如一秒的超時,然後檢查該線程是否應該完成。

3

我上次遇到類似問題時發生的一種解決方案是創建一個僅用於喚醒阻塞線程的文件(例如管道)。

這個想法是從主循環創建一個文件(或每個線程1個,因爲超時提示 - 這會讓您更好地控制哪些線程被喚醒)。所有在文件I/O上阻塞的線程都會執行select(),使用它們正在嘗試操作的文件以及由主循環創建的文件(作爲讀取的成員文件描述符集)。這應該會使所有的select()調用返回。

需要將主循環中處理此「事件」的代碼添加到每個線程。

如果主循環需要喚醒所有線程,它可以寫入文件或關閉它。


我不能肯定地說,如果這個工程,作爲重組意味着需要嘗試它消失了。

9

即使您的select()不頻繁,您的select()也可能會超時,以便在某種條件下優雅退出線程。我知道,輪詢很糟糕...

另一種替代方法是爲每個孩子都有一個管道,並將其添加到線程正在監視的文件描述符列表中。當您希望退出該子項時,從父項向管道發送一個字節。不用每個線程的管道成本輪詢。

6

取決於它如何等待IO。

如果線程處於「不間斷IO」狀態(在頂部顯示爲「D」),那麼確實沒有什麼可以做的。線程通常只是簡單地進入這個狀態,做一些事情,比如等待頁面被交換(或者需要加載,例如從mmap'd文件或共享庫等),但是一個故障(特別是NFS服務器)可能會導致它會在這個狀態下停留更長時間。

真的沒有辦法逃離這個「D」狀態。線程不會響應信號(您可以發送它們,但它們將排隊)。

如果它是一個正常的IO函數,如read(),write()或像select()或poll()這樣的等待函數,則信號將正常傳遞。

1

我總是添加與我運行之前加入,以確保該線程將是合理的時間內可連接線程功能的「」功能。當一個線程使用阻塞IO時,我嘗試利用系統來破解鎖。例如,當我使用套接字時,我將終止關閉(2)關閉(2)或關閉(2),這會導致網絡堆棧乾淨地終止它。

Linux的套接字實現是線程安全的。

0

根據不同的手冊頁,信號和線程在Linux上是一個微妙的問題。 您是否使用LinuxThreads或NPTL(如果您在Linux上)?

我不確定這一點,但我認爲信號處理程序會影響整個過程,所以要麼終止整個過程,要麼終止所有過程。

你應該使用定時選擇或輪詢,並設置一個全局標誌來終止你的線程。

13

我也會推薦使用選擇或其他一些非信號手段來終止你的線程。我們有線索的原因之一是嘗試擺脫信號瘋狂。這就是說...

通常一個人使用pthread_kill()與SIGUSR1或SIGUSR2向線程發送信號。其他建議的信號 - SIGTERM,SIGINT,SIGKILL - 具有您可能不感興趣的全過程語義。

至於發送信號時的行爲,我的猜測是它與如何處理你處理了信號。如果您沒有安裝處理程序,則會應用該信號的默認操作,但會在接收信號的線程的上下文中應用。因此,例如,SIGALRM將由您的線程「處理」,但處理將包括終止進程 - 可能不是期望的行爲。

線程接收到的信號通常會將其從EINTR讀取中分離出來,除非它確實處於前面回答中提到的那種不可中斷狀態。但我認爲這不是,或者您對SIGALRM和SIGIO的實驗不會終止這個過程。

您的閱讀也許在某種循環?如果讀取以-1返回終止,則跳出該循環並退出該線程。

你可以用這個非常草率的代碼,我放在一起測試一下我的假設玩 - 我是一對夫婦從時區,此刻我的POSIX的書拿走的......

#include <stdlib.h> 
#include <stdio.h> 
#include <pthread.h> 
#include <signal.h> 

int global_gotsig = 0; 

void *gotsig(int sig, siginfo_t *info, void *ucontext) 
{ 
     global_gotsig++; 
     return NULL; 
} 

void *reader(void *arg) 
{ 
     char buf[32]; 
     int i; 
     int hdlsig = (int)arg; 

     struct sigaction sa; 
     sa.sa_handler = NULL; 
     sa.sa_sigaction = gotsig; 
     sa.sa_flags = SA_SIGINFO; 
     sigemptyset(&sa.sa_mask); 

     if (sigaction(hdlsig, &sa, NULL) < 0) { 
       perror("sigaction"); 
       return (void *)-1; 
     } 
     i = read(fileno(stdin), buf, 32); 
     if (i < 0) { 
       perror("read"); 
     } else { 
       printf("Read %d bytes\n", i); 
     } 
     return (void *)i; 
} 

main(int argc, char **argv) 
{ 
     pthread_t tid1; 
     void *ret; 
     int i; 
     int sig = SIGUSR1; 

     if (argc == 2) sig = atoi(argv[1]); 
     printf("Using sig %d\n", sig); 

     if (pthread_create(&tid1, NULL, reader, (void *)sig)) { 
       perror("pthread_create"); 
       exit(1); 
     } 
     sleep(5); 
     printf("killing thread\n"); 
     pthread_kill(tid1, sig); 
     i = pthread_join(tid1, &ret); 
     if (i < 0) 
       perror("pthread_join"); 
     else 
       printf("thread returned %ld\n", (long)ret); 
     printf("Got sig? %d\n", global_gotsig); 

} 
0

我認爲最乾淨方法會讓線程在循環中使用條件變量來繼續。

當發生I/O事件時,應該發送條件信號。

主線程可能只是在將循環謂詞變爲false時發出信號。

類似:

while (!_finished) 
{ 
    pthread_cond_wait(&cond); 
    handleio(); 
} 
cleanup(); 

記得用條件變量來妥善處理的信號。他們可以擁有諸如「虛假喚醒」之類的東西。所以我會圍繞cond_wait函數包裝自己的函數。

0
struct pollfd pfd; 
pfd.fd = socket; 
pfd.events = POLLIN | POLLHUP | POLLERR; 
pthread_lock(&lock); 
while(thread_alive) 
{ 
    int ret = poll(&pfd, 1, 100); 
    if(ret == 1) 
    { 
     //handle IO 
    } 
    else 
    { 
     pthread_cond_timedwait(&lock, &cond, 100); 
    } 
} 
pthread_unlock(&lock); 

thread_alive是線程特定的變量,可以與信號結合使用以殺死線程。

至於你需要的句柄IO部分,以確保你用O_NOBLOCK選項打開了,或者如果它的套接字有一個類似的標誌你可以設置MSG_NOWAIT ??。對於其他fds我不知道

1

我很驚訝沒有人提出過pthread_cancel。我最近編寫了一個多線程I/O程序,並調用cancel()和join()之後工作得很好。

我最初嘗試過pthread_kill(),但最終只是用我測試過的信號來終止整個程序。

1

如果您在EINTR上封閉的第三方庫中,您可能需要考慮將pthread_kill與信號(USR1等)結合使用,調用一個空函數(而不是SIG_IGN),然後實際關閉/替換有問題的文件描述符。通過使用dup2將/ fd替換爲/ dev/null或類似文件,您將使第三方庫在重試讀取時得到文件結束結果。

請注意,通過首先dup()原始套接字,可以避免需要實際關閉套接字。

12

執行此操作的規範方法是使用pthread_cancel,其中線程已完成pthread_cleanup_push/pop以爲其正在使用的任何資源提供清理。

不幸的是,這不能在C++代碼中使用。在pthread_cancel時,調用堆棧上的任何C++ std lib代碼或ANY try {} catch()都可能會導致您的整個進程中斷。

唯一的解決辦法是處理SIGUSR1,設置一個停止標誌,pthread_kill(SIGUSR1),那麼任何地方線程被阻塞在I/O,如果你得到EINTR檢查停止標誌重試前的I/O。實際上,這在Linux上並不總是成功,不知道爲什麼。

但是在任何情況下,如果您必須調用任何第三方庫,那麼它們將毫無用處,因爲它們很可能會有一個嚴格的循環,只需重新啓動EINTR上的I/O即可。對其文件描述符進行逆向工程以關閉它也不會削減它 - 它們可能正在等待信號量或其他資源。在這種情況下,編寫工作代碼,句號根本不可能。是的,這完全是腦殘。與那些設計C++例外和pthread_cancel的人交談。據說這可能會在未來版本的C++中得到修復。祝你好運。