【问题标题】:Race conditions can also occur in traditional, single-threaded programs - Clarity竞争条件也可能发生在传统的单线程程序中 - 清晰
【发布时间】:2018-05-23 21:22:37
【问题描述】:

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

我正在阅读“PThreads programming - A Posix standard for better multiprocessing nutshell-handbook”。在第 5 章(Pthreads 和 Unix)中,作者谈到了在多线程程序中处理信号。在“线程安全库函数和系统调用”部分,作者发表了我在读过的关于并行编程的大多数书籍中没有看到的声明。声明是:

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

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

我也很想知道信号处理程序如何导致单线程程序中的RACE CONDITION

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

【问题讨论】:

  • 注意:CS 学生可能无法回答这个问题。
  • 我认为不能将其称为经典意义上的竞争条件。竞争条件具有某种随机行为,具体取决于调度程序策略和时间安排。作者可能在谈论从多个递归调用访问同一对象/资源时可能出现的错误。但这种行为是完全确定的。信号是另一回事,因为它们是异步发生的,显然可以中断某些数据处理并触发对该数据的其他处理。
  • @EugeneSh。您的评论中缺少什么,导致您不认为这是一个答案?你甚至正确地对比了这两个非常不同的方面(据我所知是正确的)。
  • @Yunnosch 缺乏100%的信心:)
  • 好的,我确信。将转换为答案。

标签: c multithreading pthreads


【解决方案1】:

我认为不能将其称为经典意义上的竞争条件。竞争条件具有某种随机行为,具体取决于调度程序策略和时间安排。

作者可能是在谈论从多个递归调用访问同一对象/资源时可能出现的错误。但是这种行为是完全确定的和可管理的。

另一方面,信号是另一回事,因为它们是异步发生的,并且显然会在中间中断一些数据处理并触发对该数据的一些其他处理,当返回到被中断的任务时会破坏它。

【讨论】:

  • 你有点自相矛盾,不是吗? - 恕我直言,这是诱导您要求存在竞争条件的随机行为的信号 - 我试图在下面的帖子中解释这一点。
【解决方案2】:

可以随时调用信号处理程序而不会发出警告,并且它可能可以访问程序中的任何全局状态。

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

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

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

【讨论】:

    【解决方案3】:

    首先了解什么是竞态条件很重要。 Wikipedia给出的定义是:

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

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


    在这个定义下,我们可以相当容易地在单线程程序中创建“虚拟”竞争条件。

    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 信号的时间。因此,信号处理引入了竞争条件。

    【讨论】:

      【解决方案4】:

      我将给出一个比你要求的更笼统的答案。这是我自己的、个人的、务实的回答,不一定要遵循“种族条件”一词的任何官方、正式的定义。

      我,我讨厌比赛条件。它们会导致大量令人讨厌的错误,这些错误难以思考、难以发现,有时甚至难以修复。所以我不喜欢做容易受到竞争条件影响的编程。所以我很少做经典的多线程编程。

      但即使我不做太多的多线程编程,我仍然会时不时地遇到某些类别,让我感觉像是竞态条件。以下是我要记住的三个:

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

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

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

      【讨论】:

      • 这三个解释都很好,很简单。谢谢。
      【解决方案5】:

      您在其中找到的书的作者似乎以一种不寻常的方式定义了“竞争条件”一词,或者他只是使用了错误的术语。

      根据通常的定义,不,递归不会在单线程程序中创建竞争条件,因为该术语是针对多个执行线程的相应操作定义的。然而,递归可能会导致某些涉及的函数不可重入。单个线程也有可能自己死锁。这些并不反映竞争条件,但作者的意思可能是其中之一或两者。

      或者,您阅读的内容可能是编辑工作不佳的结果。您引用的文本将使用信号处理和递归函数的函数组合在一起,并且信号处理程序确实可以产生数据竞争,就像多线程可以做的那样,因为信号处理程序的执行具有执行单独线程的相关特征。

      【讨论】:

      • "该术语是针对多个线程的相应操作定义的"。这并不完全正确。竞争条件的最一般定义是当程序由于时间或执行顺序的差异而导致行为不正确时。这不一定需要多个线程。
      • 很公平,@IncreasinglyIdiotic,但问题被标记为 [c],正如我所描述的,C 定义了关于多线程操作的相关术语“数据竞争”。承认这不是完全相同的术语。
      • 数据竞争实际上是竞争条件的一个子集,并且确实是针对多个线程定义的。数据竞争被定义为两个线程对同一共享内存进行无序访问,其中至少一个访问是写入。你的回答读起来就像你在说竞争条件的定义依赖于多个线程。
      【解决方案6】:

      一旦有了信号处理程序,竞争条件绝对会发生在单线程程序中。查看 pselect() 的 Unix 手册页。

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

      【讨论】:

        【解决方案7】:

        好吧,考虑下面的代码:

        #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();
        }
        

        (如果您将代码保护为thread-test.c,则使用gcc -pthread 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

        关于引文的措辞:

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

        当一个函数被调用时,一些信息会存储在进程的堆栈中——例如。退货地址。该信息称为调用帧。如果你递归地调用一个函数,就像上面的例子一样,这个信息会被多次存储在堆栈中——存储了几个调用帧。 说得有点笨拙,我承认……

        【讨论】:

        • deadlock 不是 data race
        • 作者所说的竞争条件与死锁情况有很大不同
        • 你是对的。然而,我猜引文中陈述的基本问题或多或少正是我所描述的问题。恕我直言;)
        • 因此,希望我的观点更清楚......感谢您的批评!
        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2016-10-12
        • 1970-01-01
        • 2021-06-04
        • 1970-01-01
        相关资源
        最近更新 更多