使用pthread_kill()来终止为I / O阻塞的线程的同步问题

时间:2018-11-13 13:53:09

标签: c++ c linux multithreading synchronization

以前,我曾问过question关于如何终止为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在poll()系统调用中被阻塞,则Thread2设置TerminateFlag并将信号发送到Thread1时,以上代码可以正常工作。但是,如果在Thread1的getTimeRemainedForNextEvent()函数设置ContextFlag并发送信号之后发生上下文切换,则Thread1的poll()将丢失数小时,因为它丢失了中断系统调用的信号。

似乎我不能使用互斥锁进行同步,因为poll()会保持锁直到它被解除阻塞。我可以使用任何同步机制来避免上述问题吗?

3 个答案:

答案 0 :(得分:2)

考虑在传递给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发送消息。这将导致轮询返回,然后线程1将终止。您甚至可以将此“特殊”值用作终止标志,这使逻辑变得更简洁。

答案 1 :(得分:2)

首先,通过互斥或类似的同步机制来保护多个线程must对共享变量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的答案所建议的:通过线程1正在轮询的文件描述符(即通过管道)使线程2唤醒线程1。由于您似乎有理由寻求替代方法,因此,也可以通过适当使用信号屏蔽来使您的信令方法有效。扩展@Shawn的评论,这是它的工作方式:

  1. 父线程在启动线程1之前会阻塞SIGUSR2,以便后者从其父级继承其信号掩码的线程开始时会阻塞该信号。

  2. 线程1使用ppoll()而不是poll(),以便能够指定SIGUSR2在该调用期间将不受阻塞。 ppoll()会自动进行信号掩码处理,因此在呼叫之前被阻塞并在其中被解除阻塞时,信号就不会丢失。

  3. 线程2使用pthread_kill()SIGUSR2发送到线程1以使其停止。因为只有在执行ppoll()调用时该线程的信号才被解除阻塞,所以它不会丢失(阻塞的信号将一直待处理直到解除阻塞)。正是为此设计ppoll()的一种使用场景。

  4. 您甚至应该能够消除terminateThread变量和相关联的同步,因为您应该能够依靠在ppoll()调用期间传递的信号并因此导致EINTR要执行的代码路径。该路径不依赖terminateThread来使线程停止。

答案 2 :(得分:2)

正如您自己说的那样,您可以使用线程取消来解决此问题。 在线程取消之外,我认为没有一种“正确的”方法可以在POSIX中解决此问题(用poll唤醒write调用并不是对所有人都适用的通用方法可能会阻塞线程的情况),因为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 library使用信号进行线程取消(因为信号会破坏内核模式下的长阻塞调用) 并将其与全局程序集标签结合使用,以便通过标志设置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);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来干净地解决您的问题,并且我也可以在我的个人项目中使用它来进行可靠的线程取消,而不必用pthread_cleanup_{push,pop}

这种方法的明显缺点是它不可移植,我只为x86_64 musl实现了它。我今天已经发布了它,希望有人(Cygwin,MacOSX?)复制它,因为我认为这是在C语言中进行取消的正确方法。

在C ++和glibc中,您可以利用以下事实:glibc使用异常来实现线程取消,并简单地使用pthread_cancel(在其下面使用信号(SIGCANCEL)),但是抓住它而不是让它杀死线程。 线程。


注意:

我真的在使用两个线程局部标志-一个breaker标志,如果在进入系统调用之前设置了该标志,则使用ECANCELED中断下一个系统调用(从可能长时间阻塞的系统调用返回的EINTR在修改后变成ECANCELED libc提供的syscall包装器(如果设置了中断标志)和已保存的中断标志-使用中断标志后,将其保存在已保存的中断标志中并清零,以使中断标志不会再中断可能更长的阻塞系统调用。

这个想法是取消信号被一次处理一次(信号处理程序可以保留所有/大多数信号;处理程序代码(如果有的话)可以解除阻止信号),并且正确检查代码开始展开,即,在返回错误时进行清理,直到看到ECANCELED。然后,下一个可能会长时间阻塞的syscall可能在清理代码中(例如,将</html>写到套接字的代码),并且该syscall必须是可输入的(如果Breaking标志保持打开状态,则不会)。当然,如果其中包含例如write(1,"</html>",...)的清理代码,它也可能会无限期地阻塞,但是您可以编写清理代码,以便当清理由于错误而在计时器下运行的潜在长期阻塞系统调用(ECANCELED是一个错误)。正如我已经提到的那样,强大的,无竞争条件的,信号驱动的计时器是此扩展允许的功能之一。

EINTR => ECANCELED转换发生,因此EINTR上的代码循环知道何时停止循环(许多EINTR(=信号中断了系统调用)无法阻止,并且代码应通过重试系统调用来简单地处理它们。取消为“ EINTR,之后不应重试。”