竞争条件也可能出现在传统的单线程程序中 - Clarity

时间:2018-05-23 21:22:38

标签: c multithreading pthreads

在过去的几个月里,我读过几本关于并行编程的书,我决定通过学习posix线程来关闭它。

我正在阅读“ PThreads编程 - 更好的多处理坚果壳手册的Posix标准”。在第5章(Pthreads和Unix)中,作者讨论了在多线程程序中处理信号。在“ Threadsafe库函数和系统调用”部分中,作者发表了一篇声明,我在大多数书中都没有看到我读过并行编程。声明是:

  

竞争条件也可能发生在使用信号处理程序或递归调用例程的传统单线程程序中。这种单线程程序可能在其进程堆栈的各种调用帧中具有相同的例程。

我觉得破译这句话有点乏味。当递归函数使用静态存储类型保持内部结构时,是否会出现递归函数中的竞争条件?

我也很想知道信号处理程序如何在单个程序中导致 RACE条件

注意:我不是计算机科学专业的学生,​​我非常感谢简化术语

6 个答案:

答案 0 :(得分:4)

可以在没有警告的情况下随时调用信号处理程序,它可以访问程序中的任何全局状态。

所以,假设你的程序有一些全局标志,信号处理程序设置为响应,......我不知道,... SIGINT。并且你的程序在每次调用f(x)之前检查标志。

if (! flag) {
    f(x);
}

这是一场数据竞赛。在信号发生后不能保证不会调用f(x),因为信号可能随时潜入,包括在“主”程序测试标志之后。

答案 1 :(得分:3)

首先,了解竞争条件是很重要的。 Wikipedia给出的定义是:

  

当应用程序依赖于进程或线程的顺序或时间以使其正常运行时,软件会出现竞争条件。

需要注意的重要一点是,程序可以根据时间或执行顺序正确和不正确地运行。

我们可以很容易地创建" dummy"根据这个定义,单线程程序中的竞争条件。

bool isnow(time_t then) {
    time_t now = time(0);
    return now == then;
}

上述功能是一个非常愚蠢的例子,虽然大部分都不起作用,有时它会给出正确答案。正确与不正确的行为完全取决于时间,因此表示单个线程上的竞争条件。

更进一步,我们可以编写另一个虚拟程序。

bool printHello() {
    sleep(10);
    printf("Hello\n");
}

上述程序的预期行为是在等待10秒后打印"Hello"

如果我们在调用函数后11秒发送SIGINT信号,则一切都按预期运行。如果我们在调用函数后3秒发送SIGINT信号,则程序运行不正常并且不会打印"Hello"

正确和不正确行为之间的唯一区别是SIGINT信号的时间。因此,通过信号处理引入了竞争条件。

答案 2 :(得分:2)

我会给出一个比你要求的更一般的答案。这是我自己的,个人的,务实的答案,不一定是对任何官方的,正式的术语定义"种族条件"。

我,我讨厌竞争条件。它们导致了大量难以思考,难以发现,有时难以修复的令人讨厌的错误。因此,我不喜欢编写易受竞争条件影响的节目。所以我不做太多经典的多线程编程。

但即使我不做多线程编程,我仍然会遇到某些类似于我不时感受到竞争条件的类别。以下是我试着记住的三个:

  1. 你提到的那个:信号处理程序。接收信号和调用信号处理程序是一个真正的异步事件。如果您有某种类型的数据结构,并且在信号发生时您正在修改它,并且如果您的信号处理程序也尝试修改相同的数据结构,那么您就会遇到竞争条件。如果被中断的代码正在做一些使数据结构处于不一致状态的事情,那么信号处理程序中的代码可能会混淆。还要注意,它不一定是信号处理程序中的代码,而是信号处理程序调用的任何函数,或者由信号处理程序调用的函数调用等。

  2. 共享操作系统资源,通常位于文件系统中:如果您的程序访问(或修改)文件系统中的文件或目录,这些文件或目录也被其他进程访问或修改,那么您已经获得了竞争条件的巨大潜力。 (这并不奇怪,因为在计算机科学意义上,多个进程多个线程。它们可能有单独的地址空间,这意味着它们不能以这种方式相互干扰,但显然是文件系统是一种共享资源,它们仍然可以相互干扰。)

  3. 非重入函数,例如strtok。如果函数保持内部静态,则在另一个实例处于活动状态时,您无法再次调用该函数。这不是竞争条件"在正式意义上,它有许多相同的症状,也有一些相同的修复:不使用静态数据;尝试编写你的功能,以便他们可以重入。

答案 3 :(得分:1)

你找到的这本书的作者似乎正在定义术语"竞争条件"以一种不同寻常的方式,或者他可能只是使用了错误的术语。

按照通常的定义,不,递归不会在单线程程序中创建竞争条件,因为该术语是根据多个执行线程的相应动作定义的。但是,递归可能会产生某些函数的非重入。单个线程也可以对自己进行死锁。这些并不反映竞争条件,但也许其中一个或两个都是作者的意思。

或者,您可能阅读的内容可能是编辑错误的结果。您引用的文本将信号处理与递归函数一起分组的函数,信号处理程序确实可以产生数据竞争,就像多线程可以做的那样,因为信号处理程序的执行具有执行单独线程的相关特性。 / p>

答案 4 :(得分:0)

一旦有了信号处理程序,种族条件就绝对会发生在单线程程序中。在Unix手册页上找到pselect()。

一种发生的方式是这样的:您有一个设置某些全局标志的信号处理程序。您检查全局标志,因为很明显您使系统调用挂起,因此确信在信号到达时系统调用会提前退出。但是信号仅在您检查全局标志之后且在系统调用发生之前到达。因此,现在您陷入系统调用中,等待信号已经到达。在这种情况下,竞争就在单线程代码和外部信号之间。

答案 5 :(得分:-2)

好吧,请考虑以下代码:

#include <pthread.h>

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

int num = 2;

void lock_and_call_again() {
    pthread_mutex_lock(&mutex);
    if(num > 0) {
        --num;
        lock_and_call_again();
    }
}


int main(int argc, char** argv) {
    lock_and_call_again();
}

(如果您将代码安全为gcc -pthread thread-test.c

,则使用thread-test.c进行编译

这显然是单线程的,不是吗? 从来没有,它会进入一个死锁,因为你试图锁定已经锁定的互斥锁。

这基本上是你引用的段落中的意思,恕我直言:

无论是在多个线程还是单个线程中完成,如果您尝试锁定已锁定的互斥锁,程序将以死锁结束。

如果某个函数调用自身,如上面的lock_and_call,则称之为递归调用

就像james large解释的那样,信号可以随时出现,如果信号处理程序注册了这个信号,它将在不可预测的时间调用,如果没有采取任何措施,即使同一个处理程序已经被执行 - 产生某种隐式递归执行信号处理程序。

如果此处理程序获取某种锁定,则最终会陷入死锁,即使没有明确调用自身的函数也是如此。 考虑以下功能:

pthread_mutex_t mutex;

void my_handler(int s) {

    pthread_mutex_lock(&mutex);
    sleep(10);
    pthread_mutex_unnlock(&mutex);

}

现在,如果您为特定信号注册此功能,则只要程序捕获到该信号,就会调用该函数。如果处理程序已被调用并休眠,它可能会被中断,处理程序再次被调用,并且处理程序会尝试锁定已被锁定的mutex

关于引文的措辞:

此类单线程程序可能在其进程堆栈的各种调用帧中具有相同的例程。

调用函数时,某些信息存储在进程的堆栈中 - 例如退货地址。该信息称为呼叫帧。如果以递归方式调用函数(如上例所示),则此信息会多次存储在堆栈中 - 存储多个调用帧。 据我所知,它有点笨拙......