【问题标题】:Thread safety vs Re entrancy线程安全与重入
【发布时间】:2012-02-25 03:48:35
【问题描述】:

我知道这个论坛已经讨论了很多。 但有一件事仍然让我感到困惑。 维基百科提到每个可重入代码都是线程安全的。 http://en.wikipedia.org/wiki/Reentrant_%28subroutine%29 稍后给出一个可重入但不是线程安全的函数的示例。

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

这让我很困惑。所有可重入代码都是线程安全的吗? 此外,所有递归函数都是线程安全的。 我无法想象其中的差异。

谢谢

【问题讨论】:

  • 维基文章有“问题”,恕我直言。您粘贴的上面的示例并不是真正的非线程安全的可重入函数,它是对带有故意引入错误的可重入函数的尝试。 swap() 根本不需要全局 't'。只是很傻。如果一个函数可以被中断,重新进入,(从另一个堆栈),然后为一个或两个调用返回错误的结果,它就不是可重入的!线程上下文更改是一个中断,因此可重入函数必须能够将正确的结果返回给所有调用线程。
  • @MartinJames 请参阅我的答案,了解线程安全的不可重入和可重入的非线程安全函数的示例。诚然,两者都有些做作,但我认为它们说明了这一点。

标签: multithreading operating-system


【解决方案1】:

重入和线程安全的概念是相关的,但不是等价的。您可以编写非线程安全的可重入函数和不可重入的线程安全函数。我将使用 C# 作为示例:

非线程安全的重入函数

此函数反转数组的条目:

void Reverse(int[] data) {
    if (data == null || data.Length < 2) return;
    for (var i = 0 ; i != data.Length/2 ; i++) {
        int tmp = data[i];
        data[i] = data[data.Length-i-1];
        data[data.Length-i-1] = tmp;
    }
}

这个函数显然是可重入的,因为它不引用外部资源。然而,它的线程安全是有条件的,因为它没有从多个线程传递相同的data。如果多个线程同时传递Reverse 数组的同一个实例,则可能会产生不正确的结果。使这个函数无条件线程安全的一种方法是添加一个锁:

void Reverse(int[] data) {
    if (data == null || data.Length < 2) return;
    lock (data) {
        for (var i = 0 ; i != data.Length/2 ; i++) {
            int tmp = data[i];
            data[i] = data[data.Length-i-1];
            data[data.Length-i-1] = tmp;
        }
    }
}

不可重入的线程安全函数

此函数调用函数f()c 次,并返回比其他值更多次返回的值。

static int[] counts = new int[65536];

unsigned short MaxCount(Func<unsigned short> f, int c) {
    lock(counts) {
        Array.Clear(counts, 0, counts.Length);
        for (var i = 0 ; i != c ; i++) {
            counts[f()]++;
        }
        unsigned short res = 0;
        for (var i = 1 ; i != counts.Length ; i++) {
            if (counts[i] > counts[res]) {
                res = i;
            }
        }
        return res;
    }
}

这个函数是线程安全的,因为它锁定了它用来进行计数的静态数组。但是它是不可重入的:比如传入的函子f调用MaxCount,就会返回错误的结果。

【讨论】:

  • '这个函数显然是可重入的' - 如果它被中断并且中断处理程序将相同的数组传递给它会发生什么?为这样的例程添加锁可能是灾难性的,因为锁通常不适用于中断处理程序/驱动程序 - 操作系统可能会出现双重故障停止或 BSOD。关键是,如果你使用“通过引用传递相同的参数”的说法来表示一个函数不是线程安全的,它也意味着该函数是不可重入的。
  • 恕我直言,'reentrant' 应该意味着你可以从任何地方,用任何东西调用它,并且它总是有效的。
  • @MartinJames 我的理解是,当代码可以以可重入方式使用时,代码是可重入的,也许是遵循某些规则。对于Reverse(int[]),规则是您不能传递与其参数相同的数组。相反,strtok 不能通过遵循规则使之可重入,无论您设置什么规则。即使在对其静态变量的访问周围添加锁也会使 strtok 线程安全而不使其可重入:即使在调用周围加锁,您也不能在解析 不同 字符串的两个嵌套循环中使用 strtok给它。因此,strtok 不可重入。
【解决方案2】:

所有可重入代码都是线程安全的吗?

没有。

稍后在维基百科文章中:…The key for avoiding confusion is that reentrant refers to only one thread executing. It is a concept from the time when no multitasking operating systems existed.

换句话说,重入的概念忽略了多线程执行(作为最常见的例子)。仅当这是您系统中的实际约束时,是的,可重入将被视为线程安全(该环境中也只有一个线程)。

IMO,术语“可重入”应仅用于合格系统的上下文中(不适用于您的桌面操作系统,但适用于一些嵌入式系统)。

现在,如果您真的想在多线程环境中强制使用“重入”一词:只有在保证多线程系统也是线程安全的情况下,您才能在多线程系统中强制重入。也许更好的问题是 “为了保证在多线程上下文中的重入,这是否意味着函数和它引用的所有数据也需要是线程安全的?” - A:是的,函数及其引用的所有内容也需要是线程安全的和可重入的,函数才能在该上下文中重入。快速实现这一点变得很复杂,这也是全局变量不是一个好主意的原因。

而且,所有递归函数都是线程安全的。

没有。

【讨论】:

  • 好的,我会咬的。哪些可重入代码/数据不是线程安全的?可重入函数可以被中断,从另一个上下文再次调用,并且将为被中断和中断上下文返回正确的结果。抢占式多任务处理程序是中断处理程序,那么问题出在哪里?
  • @MartinJames 我想问题是你不同意我的回答,维基百科文章中有几个点/例子,以及其他答案。
【解决方案3】:

函数“swap”不可重入,因为它使用全局状态(也就是全局变量“t”)。

【讨论】:

    【解决方案4】:

    如果您的子程序不影响外部变量(没有副作用),您可以说它可重入。虽然这不是经验法则,但它是一个很好的指南。
    如果子程序使用外部变量并在开始时保存外部变量的状态并在结束时恢复该状态,则子程序可重入

    如果一个子程序修改了外部变量并且没有先保存它们的状态,而被中断,这些外部变量的状态是可以改变的,这样当调用返回到子程序中的原始位置时,外部变量不同步,导致子程序的状态不一致。

    可重入函数保存它们的状态(在它们的本地堆栈中,在线程堆栈中,不使用全局变量)。

    在您的情况下,您访问了一个外部变量t,但它是可重入的,因为该变量在子例程结束时被保存和恢复。

    通常线程安全意味着可重入,但同样不是准则更多的规则。 注意: java 中的某些锁是可重入的,因此您可以递归调用该方法,而不会被之前的调用阻塞。 (在这种情况下,重新进入意味着线程可以访问使用相同锁锁定的任何部分 - 线程可以重新进入它已经持有锁的任何代码块)

    解决方案/答案:
    如果你用原子保护t,你应该得到一个线程安全的子例程。此外,如果您将 t 放在每个线程的堆栈上(使其成为本地),则子例程将成为线程安全的,因为没有全局数据。

    另外,可重入!=递归;您也可以在递归子例程中执行 ISR。线程方面,如果你从 2 个线程调用递归子例程,你会得到垃圾。为了使线程安全,请使用重入锁保护递归子程序(其他非重入锁将导致死锁/阻塞)。

    【讨论】:

    • '线程方面,如果你从 2 个线程调用递归子例程,你会得到垃圾'。为什么?许多递归代码仅使用堆栈帧来存储数据——仅使用局部变量和参数,没有静态/全局变量。
    • @MartinJames 我使用的示例(在我的脑海中)是递归代码可以使用一些外部变量。通常,他们不会使用它,但有可能因此它们不是线程安全的。我想我应该编辑和澄清
    【解决方案5】:

    该函数不是线程安全的,因为它访问的资源 t 在函数范围之外(未在堆栈上分配),没有任何保护机制(例如锁)来确保对 t 的访问是原子的。

    维基百科页面实际上指出:

    可重入子例程可以实现线程安全,但是这种情况 在所有情况下,单独可能不够

    (我的重点)。

    更新:

    可重入(如文章中所定义)仅表示单个执行线程(想想 DOS)可以从执行例程的中间“撕下指令指针”,将其放在其他地方,并继续线性流程新代码(例如 DOS 时代的中断子程序),同样的线性流程可以再次返回到函数中。在这种有限的情况下,函数的第二次调用必须在控制从中断子例程转移回“常规”程序执行之前完成,这将在例程中指令指针最初被撕掉的同一点恢复。这种情况不允许任意线程调度,而是允许将控制切换到新点的中断,然后完成,并从中断点恢复。

    请注意,在现实生活中,事情可能并不那么简单。我不再完全确定(已经...... 20 年?),但我认为一个中断例程可以中断并且中断例程已经在进行中(例如,软调试器中断可以中断定时器中断等)。

    【讨论】:

    • 这是否意味着每个可重入代码都不一定是线程安全的。对吗?
    • @DeeptiJain - 这是我有问题的陈述。我想不出一个非线程安全的可重入函数的例子。
    • 更新了关于线程环境和允许中断的环境之间的区别的想法。
    猜你喜欢
    • 2010-10-25
    • 1970-01-01
    • 1970-01-01
    • 2017-05-06
    • 1970-01-01
    • 2018-01-09
    • 1970-01-01
    • 1970-01-01
    • 2014-05-06
    相关资源
    最近更新 更多