【问题标题】:Why is this code reentrant but not thread-safe为什么此代码可重入但不是线程安全的
【发布时间】:2012-02-02 17:06:09
【问题描述】:

我曾经认为所有可重入函数都是线程安全的。但是我读了Reentrancy page in Wiki,它发布的代码“完全可重入,但不是线程安全的。因为它不能确保全局数据在执行期间处于一致状态”

int t;

void swap(int *x, int *y)
{
        int s;

        s = t;  // save global variable
        t = *x;
        *x = *y;
        // hardware interrupt might invoke isr() here!
        *y = t;
        t = s;  // restore global variable
}

void isr()
{
        int x = 1, y = 2;
        swap(&x, &y);
}

我不明白它的解释。为什么这个函数不是线程安全的?是不是因为在线程执行过程中会改变全局变量int t

【问题讨论】:

  • 这个例子有点做作。但是,可重入和线程安全是正交的概念。
  • Posix 对可重入有另一种定义“在 POSIX.1c 中,“可重入函数”被定义为“当被两个或多个线程调用时,其效果保证与线程一样每个人都以未定义的顺序一个接一个地执行函数,即使实际执行是交错的”,维基百科上的那个(imo。非常糟糕)示例不符合
  • 在我看来,这个例子是不可重入的:被中断的swap()不会像预期的那样交换xy所指向的值(*y可能被设置为2 *x) 的初始值。

标签: c multithreading


【解决方案1】:

这种重入的技巧是在执行第二个调用时停止执行第一个调用。就像子函数调用一样。第二次通话完全结束后,第一次通话继续。因为该函数在进入时保存 t 的状态并在退出时恢复它,所以当它继续时第一次调用没有任何变化。因此,无论第一次调用究竟在哪里中断,您始终有一个明确且严格的执行顺序。

当此函数在多个线程中运行时,所有执行都是并行完成的,即使与多核 CPU 真正并行也是如此。没有定义所有线程的执行顺序,只有在单个线程中。因此 t 的值可以随时由其他线程之一更改。

【讨论】:

  • 所以重入不包括并行?比如foo() {a();b()},当重入发生时,只能是a(a b) b,但不能是a a b b 多线程可以吗?
  • 基本上是的。这里给出的重入总是 (a2 b2),因为内部调用总是在外部调用继续之前完全执行。在多线程中,它取决于线程调度,也可以是 a1 a2 b1 b2。使函数可重入并不意味着它是线程安全的。另一种方式:使其线程安全并不意味着它是可重入的。两者都必须分开处理,这始终是问题(即变量)是否以及如何在调用之间和线程之间共享。
  • 仅供参考。对我来说,真的很难想象“只能从同一个线程重新进入”的情况。因为我害怕 heisenbug,所以我训练自己暗示并行执行线程的东西。而且我只从线程相关的文本中看到了术语reentrancy。我认为对于在抢占式多线程变得普遍后开始编程的人们来说也是如此。但具有讽刺意味的是,这是理解该术语的关键。
  • 优秀的答案。改进了 wikipedia 条目,虽然不错,但无法解释这一点。
【解决方案2】:

为了给出更通用的答案,重入仅在函数级别。这意味着函数的一次调用不会改变可以改变第二次调用的功能的状态。

在给出的示例中,全局变量在函数的两次调用之间没有改变。函数内部发生的事情对函数的每次调用没有影响。

一个不可重入函数的例子是strtok

例如,不可能用它嵌套 2 个解析循环:

 /* To read a several lines of comma separated numbers */
 char buff[WHATEVER], *p1, *p2;

  p1 = strtok(buff, "\n");
  while(p1) {
    p2 = strtok(p1, ",");
    while(p2) {
      atoi(p2);
      p2 = strtok(NULL, ",");
      }
    }
    p1 = strtok(NULL, "\n");
  }

这不起作用,因为第二次调用破坏了外部 strtok 循环的状态(必须使用可重入变体 strtok_r)。

【讨论】:

  • 问题是要求解释可重入函数如何不是线程安全的。这并没有回答问题,它只是提供了一个相反的例子。
  • knowledg's source 添加到此...
【解决方案3】:

假设线程 A 和线程 B。线程 A 有两个局部变量 a = 5,b = 10,线程 B 有两个局部变量 p = 20,q = 30。

线程 A 调用:swap (&a, &b) ;

线程 B 调用:swap (&p, &q) ;

我假设两个线程都在不同的内核上运行并且属于同一个进程。 变量 t 是全局的,而 int x, int y 对于给定的函数是局部的。以下线程调度显示了“t”的值如何根据线程调度而变化,从而使代码线程不安全。说全局 t = 100;

 Thread A         Thread B
 1) int s;        int s;
 2) s = 100;      s = 100;
 3) t = 5;        no operation(nop);
 4) nop;          t = 20;  // t is global so Thread A also sees the value as t = 20
 5) x = 10;       x = 30;
 6) y = 20;       y = 20;  // Thread A exchange is wrong, Thread B exchange is OK  

现在试着想象一下如果上面的语句 3 和 4 的顺序不同会发生什么。然后 t 将获得值 5 并且线程 B 中的交换将是错误的。如果两个线程在同一个处理器上,情况就更容易了。那么上述所有操作都不是同时进行的。我刚刚在第 3 步和第 4 步中展示了交错,因为这些是最重要的。

【讨论】:

    【解决方案4】:

    我将尝试提供另一个(可能不那么做作的)函数示例,该示例是可重入的,但不是线程安全的。

    这是“河内塔”的一个实现,使用共享的全局“临时”堆栈:

    stack_t tmp;
    
    void hanoi_inner(stack_t src, stack_t dest, stack_t tmp, int n)
    {
       if (n == 1) move(src, dest)
       else {
         hanoi_inner(src, tmp, dest, n - 1);
         move(src, dest);
         hanoi_inner(tmp, dest, src, n - 1);
       }
    }
    
    void hanoi(stack_t src, stack_t dest, int n) { hanoi_inner(src, dest, tmp, n); }
    

    函数hanoi() 是可重入的,因为它在返回时保持全局缓冲区tmp 的状态不变(一个警告:在重入期间可能会违反在tmp 上增加磁盘大小的通常约束调用。)但是hanoi() 不是线程安全的。

    如果增量运算符n++ 是原子的,则这是一个线程安全且可重入的示例:

    int buf[MAX_SIZE];  /* global, shared buffer structure */
    int n;              /* global, shared counter */
    
    int* alloc_int() { return &buf[n++]; }
    

    您真的可以将它用作一个整数单元的分配器(不检查溢出;我知道)。如果n++ 不是原子操作,那么两个线程或两个可重入调用很容易最终被分配到同一个单元格。

    【讨论】:

    • 只有当 n++ 是原子的(通常不是)时,这个才是可重入的。否则,两个调用最终都可能返回相同的指针。
    • @per 谢谢 - 我将尝试将其更改为可重入,而不使用锁定或原子 n++
    【解决方案5】:

    如果你有 2 个实例(每个在不同的线程上)执行它,一个可能会踩到另一个的脚趾:如果一个在“硬件中断”注释处被打断,另一个被执行,它可能会改变 t,以便切换回到第一个会产生不正确的结果。

    【讨论】:

      【解决方案6】:

      因此,由于某种奇怪的原因,函数与名为 t 的全局变量混淆了。如果同时从两个不同的线程调用此函数,您可能会得到意想不到的错误结果,因为一个实例会覆盖 t 中由另一个实例写入的值。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2018-04-12
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多