237

大多数 时候,重入 定义引用自维基百科

如果计算机程序或例程在其先前的调用完成之前可以 安全地再次调用(即可以安全地同时执行),则将其描述为可重入的。可重入,计算机程序或例程:

  1. 必须不保存静态(或全局)非常量数据。
  2. 不得将地址返回给静态(或全局)非常量数据。
  3. 必须仅对调用者提供给它的数据起作用。
  4. 不能依赖锁来锁定单例资源。
  5. 不得修改自己的代码(除非在自己独特的线程存储中执行)
  6. 不得调用不可重入的计算机程序或例程。

如何安全定义?

如果一个程序可以安全地并发执行,是否总是意味着它是可重入的?

在检查我的代码的可重入能力时,我应该牢记的六点之间的共同点到底是什么?

还,

  1. 所有递归函数都是可重入的吗?
  2. 所有线程安全函数都是可重入的吗?
  3. 所有递归和线程安全的函数都是可重入的吗?

在写这个问题的时候,我想到了一件事:像重入线程安全这样的术语是绝对的吗,即它们有固定的具体定义吗?因为,如果他们不是,这个问题就没有多大意义。

4

8 回答 8

224
于 2010-05-09T21:37:03.467 回答
23

“安全”的定义完全符合常识的要求——它的意思是“正确地做它的事情而不干扰其他事情”。您引用的六点非常清楚地表达了实现这一目标的要求。

您的 3 个问题的答案是 3ד否”。


所有递归函数都是可重入的吗?

不!

例如,如果递归函数的两个同时调用访问相同的全局/静态数据,它们很容易相互搞砸。


所有线程安全函数都是可重入的吗?

不!

如果一个函数在并发调用时不会发生故障,那么它就是线程安全的。但这可以通过使用互斥锁来阻止第二次调用的执行直到第一次调用完成,因此一次只能进行一次调用。重入意味着在不干扰其他调用的情况下同时执行


所有递归和线程安全的函数都是可重入的吗?

不!

看上面。

于 2010-05-09T20:21:19.837 回答
14

共同话题:

如果例程在中断时被调用,行为是否定义良好?

如果你有这样的功能:

int add( int a , int b ) {
  return a + b;
}

那么它不依赖于任何外部状态。行为定义明确。

如果你有这样的功能:

int add_to_global( int a ) {
  return gValue += a;
}

结果在多个线程上没有很好地定义。如果时机不对,信息可能会丢失。

可重入函数的最简单形式是专门对传递的参数和常量值进行操作的东西。其他任何东西都需要特殊处理,或者通常是不可重入的。当然,参数不能引用可变的全局变量。

于 2010-05-09T20:23:28.367 回答
7

现在我必须详细说明我之前的评论。@paercebal 答案不正确。在示例代码中,没有人注意到应该作为参数的互斥锁实际上并没有传入?

我对这个结论提出异议,我断言:要使函数在存在并发的情况下安全,它必须是可重入的。因此并发安全(通常写成线程安全)意味着可重入。

线程安全和可重入都没有关于参数的任何内容:我们正在讨论函数的并发执行,如果使用不适当的参数,它仍然可能是不安全的。

例如, memcpy() 是线程安全且可重入的(通常)。显然,如果使用来自两个不同线程的指向相同目标的指针调用它,它将无法按预期工作。这就是 SGI 定义的重点,将责任放在客户端上,以确保对相同数据结构的访问由客户端同步。

重要的是要理解,通常让线程安全操作包含参数是无意义的。如果你做过任何数据库编程,你就会明白。什么是“原子的”并且可能受互斥锁或其他技术保护的概念必然是用户概念:在数据库上处理事务可能需要多次不间断的修改。除了客户端程序员,谁能说哪些需要保持同步?

关键是“损坏”不必用非序列化的写入来弄乱计算机上的内存:即使所有单独的操作都被序列化,损坏仍然可能发生。因此,当您询问一个函数是线程安全的还是可重入的时,这个问题意味着所有适当分离的参数:使用耦合参数并不构成反例。

那里有许多编程系统:Ocaml 就是其中之一,我认为 Python 也是如此,其中包含许多不可重入代码,但它使用全局锁来交错线程访问。这些系统不是可重入的,也不是线程安全的或并发安全的,它们安全运行仅仅是因为它们阻止了全局并发。

一个很好的例子是malloc。它不是可重入的,也不是线程安全的。这是因为它必须访问全局资源(堆)。使用锁并不能保证安全:它绝对不能重入。如果 malloc 的接口设计得当,就可以使其可重入和线程安全:

malloc(heap*, size_t);

现在它可以安全了,因为它将对单个堆的共享访问序列化的责任转移给了客户端。特别是如果有单独的堆对象,则不需要任何工作。如果使用公共堆,则客户端必须序列化访问。在函数内部使用锁是不够的:只需考虑一个 malloc 锁定堆*,然后出现一个信号并在同一个指针上调用 malloc:死锁:信号无法继续,客户端也不能,因为它被打断。

一般来说,锁不会使事情成为线程安全的......它们实际上通过不恰当地尝试管理客户端拥有的资源来破坏安全性。锁定必须由对象制造商完成,这是唯一知道创建了多少对象以及如何使用它们的代码。

于 2010-12-10T05:36:26.493 回答
4

列出的要点中的“公共线程”(双关语!?)是该函数不得执行任何会影响对同一函数的任何递归或并发调用的行为的任何事情。

因此,例如静态数据是一个问题,因为它由所有线程拥有;如果一个调用修改了一个静态变量,所有线程都会使用修改后的数据,从而影响它们的行为。自修改代码(虽然很少遇到,并且在某些情况下被阻止)会是一个问题,因为虽然有多个线程,但代码只有一个副本;代码也是必不可少的静态数据。

本质上是可重入的,每个线程必须能够像唯一用户一样使用该函数,如果一个线程可以以非确定性方式影响另一个线程的行为,则情况并非如此。这主要涉及每个线程具有该函数所处理的单独或恒定数据。

话虽如此,第(1)点不一定是正确的;例如,您可以合法地并通过设计使用静态变量来保留递归计数以防止过度递归或分析算法。

线程安全函数不必是可重入的;它可以通过专门防止带有锁的重入来实现线程安全,并且第 (6) 点说这样的函数是不可重入的。关于第 (6) 点,调用锁的线程安全函数的函数在递归中使用是不安全的(它将死锁),因此不能说是可重入的,尽管它可能对并发安全,并且仍然是可重入的,因为多个线程可以同时在这样的函数中拥有它们的程序计数器(只是没有锁定区域)。可能这有助于区分线程安全和重入(或者可能会增加你的困惑!)。

于 2010-05-09T20:47:46.190 回答
3

您的“也”问题的答案是“否”、“否”和“否”。仅仅因为一个函数是递归的和/或线程安全的,它不会使其可重入。

这些类型的功能中的每一种都可能在您引用的所有点上失败。(虽然我不是 100% 确定第 5 点)。

于 2010-05-09T20:19:40.947 回答
1

术语“线程安全”和“可重入”仅意味着它们的定义所表达的含义。在这种情况下,“安全”意味着您在下面引用的定义所说的内容。

这里的“安全”当然并不意味着更广泛意义上的安全,即在给定上下文中调用给定函数不会完全影响您的应用程序。总而言之,一个函数可能会在您的多线程应用程序中可靠地产生所需的效果,但根据定义不符合可重入或线程安全的条件。相反,您可以通过在多线程应用程序中产生各种不希望的、意外和/或不可预测的影响的方式调用可重入函数。

递归函数可以是任何东西,并且可重入的定义比线程安全的定义更强大,因此您编号问题的答案都是否定的。

阅读可重入的定义,人们可能会将其概括为一种功能,除了您称之为修改的内容之外,它不会修改任何内容。但你不应该只依赖摘要。

在一般情况下,多线程编程非常困难。知道代码的哪一部分可重入只是这个挑战的一部分。线程安全不是附加的。与其尝试将可重入函数拼凑起来,不如使用整体线程安全 设计模式并使用该模式来指导您使用程序中的每个线程和共享资源。

于 2010-05-10T01:19:13.650 回答
1
  • 不可重入函数意味着会有一个静态上下文,由函数维护。第一次进入时,将为您创建新的上下文。并且下次进入时,您不要为此发送更多参数,以方便进行令牌分析,. 例如 c 中的 strtok。如果您没有清楚上下文,可能会有一些错误。
/* strtok example */
#include <stdio.h>
#include <string.h>

int main ()
{
  char str[] ="- This, a sample string.";
  char * pch;
  printf ("Splitting string \"%s\" into tokens:\n",str);
  pch = strtok (str," ,.-");
  while (pch != NULL)
  {
    printf ("%s\n",pch);
    pch = strtok (NULL, " ,.-");
  }
  return 0;
}
  • 与不可重入相反,可重入函数意味着在任何时候调用函数都会得到相同的结果而没有副作用。因为没有上下文。
  • 从线程安全的角度来看,这只是意味着在当前时间,在当前进程中,公共变量只有一次修改。所以你应该添加锁保护以确保一次只对公共字段进行一次更改。
  • 所以线程安全和可重入在不同的视图中是两个不同的东西。可重入函数安全说你应该在下次进行上下文分析之前清除上下文。线程安全说你应该保持访问公共字段的顺序。
于 2021-02-08T02:46:18.830 回答