为什么这段代码是可重入的,但不是线程安全的

时间:2012-02-02 17:06:09

标签: c multithreading

我曾经认为所有可重入的函数都是线程安全的。但我读了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吗?

6 个答案:

答案 0 :(得分:6)

这种类型的重入的技巧是第一次调用的执行在执行第二次调用时停止。就像一个子功能调用。第二个呼叫完成后,第一个呼叫继续。因为该函数在进入时保存t的状态并在退出时恢复它,所以当第一次调用继续时,没有任何改变。因此,无论第一次调用在哪里被中断,您始终都有一个已定义且严格的执行顺序。

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

答案 1 :(得分:4)

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

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

非重入函数的一个示例是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)。

答案 2 :(得分: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步中展示了交错,因为这些是最重要的。

答案 3 :(得分:2)

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

以下是“河内之塔”的实施,使用共享的全球“临时”堆栈:

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++不是原子操作,则两个线程或两个可重入的调用很容易最终被分配到同一个单元格。

答案 4 :(得分:1)

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

答案 5 :(得分:0)

因此,函数会因为一些奇怪的原因而使用名为t的全局变量进行混乱。如果同时从两个不同的线程调用此函数,则可能会出现意外的错误结果,因为一个实例将覆盖另一个实例写入的t中的值。