簡體   English   中英

使用pthread_kill()來終止阻塞I / O的線程的同步問題

[英]Synchronization issue with usage of pthread_kill() to terminate thread blocked for I/O

以前我問了一個關於如何終止阻塞I / O的線程的問題 考慮到一些優點,我使用了pthread_kill()而不是pthread_cancel()或寫入管道。

我已經使用pthread_kill()實現了將信號(SIGUSR2)發送到目標線程的代碼。 下面是這個的骨架代碼。 大多數時候getTimeRemainedForNextEvent()返回一個阻止poll()幾個小時的值。 由於這個超大的超時值,即使Thread2設置了terminateFlag(以停止Thread1),Thread2也會被阻塞,直到Thread1的poll()返回(如果套接字上沒有事件,則可能在幾個小時之后)。 所以我使用pthread_kill()向Thread1發送信號以中斷poll()系統調用(如果它被阻止)。

static void signalHandler(int signum) {
    //Does nothing
}

// Thread 1 (Does I/O operations and handles scheduler events). 

void* Thread1(void* args) {
    terminateFlag = 0;
    while(!terminateFlag) {
        int millis = getTimeRemainedForNextEvent(); //calculate maximum number of milliseconds poll() can block.

        int ret = poll(fds,numOfFDs,millis);
        if(ret > 0) {
            //handle socket events.
        } else if (ret < 0) {
            if(errno == EINTR)
                perror("Poll Error");
            break;
        }

        handleEvent();  
    }
}

// Thread 2 (Terminates Thread 1 when Thread 1 needs to be terminated)

void* Thread2(void* args) {
    while(1) {

    /* Do other stuff */

    if(terminateThread1) {
            terminateFlag = 1;
            pthread_kill(ftid,SIGUSR2); //ftid is pthread_t variable of Thread1
            pthread_join( ftid, NULL );
        }
    }

    /* Do other stuff */
} 

如果Thread2設置terminateFlag並且在poll()系統調用中阻塞時向Thread1發送信號,則上面的代碼可以正常工作。 但是,如果在getTimeRemainedForNextEvent()和Thread2的getTimeRemainedForNextEvent()函數之后發生上下文切換,則設置terminateFlag並發送信號,則Thread1的poll()會因為丟失中斷系統調用的信號而被阻塞幾個小時。

似乎我不能使用互斥鎖進行同步,因為poll()將保持鎖定直到它被解除阻塞。 是否有任何同步機制可以應用以避免上述問題?

考慮在傳遞給poll的fds集合中有一個額外的文件描述符,其唯一的工作是在你想要終止線程時返回poll

因此,在第2個主題中我們會有類似的東西:

if (terminateThread1) {
        terminateFlag = 1;
        send (terminate_fd, " ", 1, 0);
        pthread_join (ftid, NULL);
    }
}

terminate_fd將在由線程1傳遞給poll的fds集合中。

- 要么 -

如果每個線程有一個額外的fd的開銷太大(如評論中所討論的那樣),那么就向線程1忽略的現有fds之一發送一些內容。 這將導致poll返回,然后線程1將終止。 你甚至可以將這個'特殊'值作為終止標志,這使得邏輯更加整潔。

首先,多個線程對共享變量terminateFlag的訪問必須受到互斥鎖或類似同步機制的保護,否則您的程序將不符合並且所有投注都將關閉。 例如,這可能是這樣的:

void *Thread1(void *args) {
    pthread_mutex_lock(&a_mutex);
    terminateFlag = 0;
    while(!terminateFlag) {
        pthread_mutex_unlock(&a_mutex);

        // ...

        pthread_mutex_lock(&a_mutex);
    }
    pthread_mutex_unlock(&a_mutex);
}

void* Thread2(void* args) {
    // ...

    if (terminateThread1) {
        pthread_mutex_lock(&a_mutex);
        terminateFlag = 1;
        pthread_mutex_unlock(&a_mutex);
        pthread_kill(ftid,SIGUSR2); //ftid is pthread_t variable of Thread1
        pthread_join( ftid, NULL );
    }

    // ...
} 

但這並沒有解決主要問題 ,線程2發送的信號在測試terminateFlag但在調用poll()之前可能會被傳遞給線程1,盡管它確實縮小了可能發生這種情況的窗口。

最干凈的解決方案是由@PaulSanders的回答建議的:讓線程2通過文件描述符喚醒線程1線程1正在輪詢(即通過管道)。 但是,由於您似乎有合理的理由尋求替代方法,因此也應該可以通過適當使用信號屏蔽來使您的信令方法起作用。 擴展@ Shawn的評論,以下是它的工作方式:

  1. 父線程在啟動線程1之前阻塞SIGUSR2 ,以便后者從其父線程繼承其信號掩碼,從阻塞的信號開始。

  2. 線程1使用ppoll()而不是poll() ,以便能夠指定SIGUSR2在該調用期間將被解除阻塞。 ppoll()確實以原子方式處理掩碼處理,因此當在呼叫之前阻塞信號並且在其內部解鎖時,沒有機會丟失信號。

  3. 線程2使用pthread_kill()SIGUSR2發送到線程1以使其停止。 因為該信號在執行ppoll()調用時僅對該線程解除阻塞,所以它不會丟失(阻塞信號在未阻塞之前保持掛起狀態)。 這正是設計ppoll()的那種使用場景。

  4. 您甚至應該能夠取消terminateThread變量和相關的同步,因為您應該能夠依賴於在ppoll()調用期間傳遞的信號,從而導致執行EINTR代碼路徑。 該路徑不依賴terminateThread來使線程停止。

正如你自己所說,你可以使用線程取消來解決這個問題。 在線程取消之外,我認為在POSIX中有一種“正確”的方法來解決這個問題(使用write喚醒poll調用並不是一種通用方法,可以適用於線程可能被阻塞的所有情況),因為POSIX的系統調用和處理信號的范例不允許你縮小標志檢查和可能長的阻塞調用之間的差距。

void handler() { dont_enter_a_long_blocking_call_flg=1; }
int main()
{  //...
    if(dont_enter_a_long_blocking_call_flg)
        //THE GAP; what if the signal arrives here ?
        potentially_long_blocking_call();
    //....
}

musl libc庫使用信號進行線程取消(因為信號可以破壞處於內核模式的長阻塞調用)並且它將它們與全局程序集標簽結合使用,以便從標志設置SIGCANCEL處理程序中,它可以做到(概念上,我我沒有粘貼他們的實際代碼:

void sigcancel_handler(int Sig, siginfo_t *Info, void *Uctx)
{
    thread_local_cancellation_flag=1;
    if_interrupted_the_gap_move_Program_Counter_to_start_cancellation(Uctx);
}

現在,如果你改變了if_interrupted_the_gap_move_Program_Counter_to_start_cancellation(Uctx); to if_interrupted_the_gap_move_Program_Counter_to_make_the_syscall_fail(Uctx); 並導出if_interrupted_the_gap_move_Program_Counter_to_make_the_syscall_fail函數以及thread_local_cancellation_flag

然后你可以用它來*:

  • 解決您的問題,可以使用任何信號強大地實現強大的信號取消,而無需將任何pthread_cleanup_{push,pop}內容放入已經正常工作的線程安全的singel線程代碼中
  • 即使信號得到處理,也要確保對目標線程中的信號傳遞保持正常的上下文反應。

基本上沒有像這樣的libc擴展,如果你曾經使用它處理的信號kill()/pthread_kill()一個進程/線程,或者如果在信號發送計時器上放置一個函數,你就無法確定對信號的確定反應交付,因為目標可能會在上面的間隙中收到信號並無限期掛起而不是響應它。

我已經在musl libc上實現了這樣一個libc擴展,現在發布它https://github.com/pskocik/musl SIGNAL_EXAMPLES目錄還顯示了一些kill()pthread_killsetitimer()示例,這些示例在已證明的競爭條件下與經典libcs​​掛起但不具備擴展的musl。 您可以使用擴展的musl來干凈地解決您的問題,我也可以在我的個人項目中使用它來執行強大的線程取消,而不必使用pthread_cleanup_{push,pop}來丟棄我的代碼

這種方法的明顯缺點是它不可移植,我只為x86_64 musl實現了它。 我今天發布了它,希望有人(Cygwin,MacOSX?)復制它,因為我認為這是在C中取消的正確方法。

在C ++和glibc中,你可以利用glibc使用異常來實現線程取消的事實,並簡單地使用pthread_cancel (它使用下面的信號(SIGCANCEL))但是捕獲它而不是讓它殺死線程。


注意:

我真的使用兩個線程本地標志 - 一個斷路器標志,如果在輸入系統調用之前設置,則使用ECANCELED中斷下一個系統調用(從一個可能長時間阻塞的系統調用返回的EINTR在修改后的libc中被轉換為ECANCELED syscall包裝器iff設置了中斷標志)和一個保存的中斷標志 - 在使用中斷標志的那一刻它被保存在保存的中斷標志中並歸零,這樣中斷標志不會打破進一步可能長時間阻塞的系統調用。

這個想法是一次一個地處理取消信號(信號處理程序可以保留所有/大多數信號被阻止;處理程序代碼(如果有的話)然后可以解鎖它們)並且正確檢查代碼開始展開,即清理在返回錯誤的同時,它看到了ECANCELED。 然后,下一個可能很長的阻塞系統調用可以在清理代碼中(例如,將</html>寫入套接字的代碼),並且系統調用必須是可輸入的(如果中斷標志保持打開,則不會)。 當然,如果清理代碼中包含例如write(1,"</html>",...) ,它也可以無限期地阻塞,但是你可以編寫清理代碼,以便在那里運行可能長時間阻塞的系統調用清理是由於錯誤導致的計時器(ECANCELED是錯誤)。 正如我已經提到的,強大的,無競爭條件的信號驅動定時器是此擴展允許的事情之一。

發生EINTR => ECANCELED轉換,以便EINTR上的代碼循環知道何時停止循環(許多EINTR(=信號中斷系統調用)無法阻止,代碼應該通過重試系統調用來處理它們。我使用ECANCELED作為“EINTR之后你不應該重試。”

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM