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

A computer program or routine is described as reentrant if it can be safely called again before its previous invocation has been completed (i.e it can be safely executed concurrently). To be reentrant, a computer program or routine: Must hold no static (or global) non-constant data. Must not return the address to static (or global) non-constant data. Must work only on the data provided to it by the caller. Must not rely on locks to singleton resources. Must not modify its own code (unless executing in its own unique thread storage) Must not call non-reentrant computer programs or routines.

如何定义安全?

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

在检查代码的重入功能时,我应该记住的这六点之间的共同点究竟是什么?

同时,

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


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


当前回答

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

比如静态数据就是个问题因为它属于所有线程;如果一个调用修改了一个静态变量,那么所有线程都会使用修改后的数据,从而影响它们的行为。自修改代码(虽然很少遇到,在某些情况下被阻止)将是一个问题,因为尽管有多个线程,但只有一个代码副本;代码也是基本的静态数据。

从本质上讲,要实现可重入,每个线程必须能够使用函数,就像它是唯一的用户一样,如果一个线程可以以不确定的方式影响另一个线程的行为,则情况就不是这样了。这主要涉及到每个线程都有函数所处理的独立数据或常量数据。

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

A thread-safe function need not be reentrant; it may achieve thread safety by specifically preventing reentrancy with a lock, and point (6) says that such a function is not reentrant. Regarding point (6), a function that calls a thread-safe function that locks is not safe for use in recursion (it will dead-lock), and is therefore not said to be reentrant, though it may nonetheless safe for concurrency, and would still be re-entrant in the sense that multiple threads can have their program-counters in such a function simultaneously (just not with the locked region). May be this helps to distinguish thread-safety from reentarncy (or maybe adds to your confusion!).

其他回答

你的“同样”问题的答案是“不”,“不”和“不”。仅仅因为函数是递归的和/或线程安全的,并不意味着它是可重入的。

每一种类型的函数都可能在你引用的所有点上失败。(虽然我不是100%确定第五点)。

“安全”的定义完全符合常识——它意味着“正确地做自己的事情,而不干扰其他事情”。你提到的六点很清楚地表达了实现这一目标的要求。

你的三个问题的答案是3ד不”。


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

NO!

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


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

NO!

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


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

NO!

见上图。

共同点是:

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

如果你有这样一个函数:

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

这样它就不依赖于任何外部状态。行为定义良好。

如果你有这样一个函数:

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

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

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

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

比如静态数据就是个问题因为它属于所有线程;如果一个调用修改了一个静态变量,那么所有线程都会使用修改后的数据,从而影响它们的行为。自修改代码(虽然很少遇到,在某些情况下被阻止)将是一个问题,因为尽管有多个线程,但只有一个代码副本;代码也是基本的静态数据。

从本质上讲,要实现可重入,每个线程必须能够使用函数,就像它是唯一的用户一样,如果一个线程可以以不确定的方式影响另一个线程的行为,则情况就不是这样了。这主要涉及到每个线程都有函数所处理的独立数据或常量数据。

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

A thread-safe function need not be reentrant; it may achieve thread safety by specifically preventing reentrancy with a lock, and point (6) says that such a function is not reentrant. Regarding point (6), a function that calls a thread-safe function that locks is not safe for use in recursion (it will dead-lock), and is therefore not said to be reentrant, though it may nonetheless safe for concurrency, and would still be re-entrant in the sense that multiple threads can have their program-counters in such a function simultaneously (just not with the locked region). May be this helps to distinguish thread-safety from reentarncy (or maybe adds to your confusion!).

1. 如何定义安全?

语义。在这种情况下,这不是一个严格定义的术语。它只是意味着“你可以这样做,没有风险”。

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

No.

例如,让我们有一个c++函数,它同时接受锁和回调作为参数:

#include <mutex>

typedef void (*callback)();
std::mutex m;

void foo(callback f)
{
    m.lock();
    // use the resource protected by the mutex

    if (f) {
        f();
    }

    // use the resource protected by the mutex
    m.unlock();
}

另一个函数很可能需要锁定同一个互斥量:

void bar()
{
    foo(nullptr);
}

乍一看,一切似乎都很好,但等等:

int main()
{
    foo(bar);
    return 0;
}

如果互斥锁不是递归的,那么在主线程中会发生这样的情况:

Main将调用foo。 Foo将获得锁。 Foo会调用bar, bar会调用Foo。 第二个foo将尝试获取锁,失败并等待它被释放。 死锁。 哎呀……

我作弊了,用回调函数。但是很容易想象更复杂的代码段具有类似的效果。

3.在检查代码的重入功能时,我应该记住的这六点之间的共同点究竟是什么?

如果函数具有/提供对可修改持久资源的访问权限,或者具有/提供对具有气味的函数的访问权限,则可以发现问题。

(好吧,99%的代码都应该有味道,然后……请参阅上一节来处理…)

所以,在研究你的代码时,有一点应该提醒你:

函数有一个状态(即访问一个全局变量,甚至是一个类成员变量) 这个函数可以被多个线程调用,也可以在进程执行时在堆栈中出现两次(也就是说,函数可以直接或间接地调用自己)。将回调函数作为参数的函数很臭。

请注意,不可重入性是病毒式传播的:可以调用可能的不可重入函数的函数不能被认为是可重入的。

还要注意的是,c++方法之所以有这种味道,是因为它们可以访问它,所以您应该研究一下代码,以确保它们没有有趣的交互。

4.1. 是否所有递归函数都是可重入的?

No.

在多线程的情况下,访问共享资源的递归函数可能同时被多个线程调用,从而导致坏的/损坏的数据。

在单线程情况下,递归函数可以使用不可重入函数(如臭名昭著的strtok),或者使用全局数据而不处理数据已经在使用的事实。你的函数是递归的,因为它直接或间接地调用自己,但它仍然是递归不安全的。

4.2. 是否所有线程安全的函数都是可重入的?

在上面的例子中,我展示了一个明显的线程安全函数是如何不可重入的。因为回调参数,我作弊了。但是,通过让线程获得两次非递归锁,可以有多种方法使线程死锁。

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

如果你说的“递归”是指“递归安全”,我会说“是的”。

如果可以保证一个函数可以被多个线程同时调用,并且可以直接或间接地调用自身,没有问题,那么它就是可重入的。

问题是如何评估这个担保…^_^

5. 像重入和线程安全这样的术语是绝对的吗?也就是说,它们有固定的具体定义吗?

我相信它们是这样的,但是,计算一个函数是线程安全的还是可重入的可能会很困难。这就是为什么我在上面使用术语嗅觉:您可以发现一个函数是不可重入的,但是很难确定一段复杂的代码是可重入的

6. 一个例子

假设你有一个对象,其中一个方法需要使用资源:

struct MyStruct
{
    P * p;

    void foo()
    {
        if (this->p == nullptr)
        {
            this->p = new P();
        }

        // lots of code, some using this->p

        if (this->p != nullptr)
        {
            delete this->p;
            this->p = nullptr;
        }
    }
};

第一个问题是,如果这个函数以某种方式递归调用(即这个函数直接或间接地调用自己),代码可能会崩溃,因为this->p将在最后一次调用结束时被删除,并且仍然可能在第一次调用结束前被使用。

因此,这段代码不是递归安全的。

我们可以使用引用计数器来纠正这一点:

struct MyStruct
{
    size_t c;
    P * p;

    void foo()
    {
        if (c == 0)
        {
            this->p = new P();
        }

        ++c;
        // lots of code, some using this->p
        --c;

        if (c == 0)
        {
            delete this->p;
            this->p = nullptr;
        }
    }
};

这样,代码就变得递归安全了…但由于多线程问题,它仍然不能重入:我们必须确保对c和p的修改将使用递归互斥锁(不是所有的互斥锁都是递归的)来原子地完成:

#include <mutex>

struct MyStruct
{
    std::recursive_mutex m;
    size_t c;
    P * p;

    void foo()
    {
        m.lock();

        if (c == 0)
        {
            this->p = new P();
        }

        ++c;
        m.unlock();
        // lots of code, some using this->p
        m.lock();
        --c;

        if (c == 0)
        {
            delete this->p;
            this->p = nullptr;
        }

        m.unlock();
    }
};

当然,这一切都假定大量代码本身是可重入的,包括p的使用。

上面的代码甚至不是异常安全的,但这是另一个故事…^_^

7. 嘿,99%的代码是不可重入的!

对于意大利式代码来说,这是非常正确的。但是如果正确划分代码,就可以避免重入问题。

7.1. 确保所有函数都有NO状态

它们必须只使用参数、自己的局部变量、其他没有状态的函数,如果要返回数据,则返回数据的副本。

7.2. 确保你的对象是“递归安全的”

一个对象方法可以访问它,所以它与对象的同一个实例的所有方法共享一个状态。

因此,确保对象可以在堆栈中的一个点上使用(即调用方法A),然后,在另一个点上使用(即调用方法B),而不会破坏整个对象。设计您的对象,以确保在退出方法时,对象是稳定和正确的(没有悬空指针,没有矛盾的成员变量,等等)。

7.3. 确保所有对象都被正确封装

任何人都不能访问他们的内部数据:

    // bad
    int & MyObject::getCounter()
    {
        return this->counter;
    }

    // good
    int MyObject::getCounter()
    {
        return this->counter;
    }

    // good, too
    void MyObject::getCounter(int & p_counter)
    {
        p_counter = this->counter;
    }

如果用户检索了数据的地址,即使返回一个const引用也可能是危险的,因为代码的其他部分可以在不告知持有const引用的代码的情况下修改它。

7.4. 确保用户知道您的对象不是线程安全的

因此,用户有责任使用互斥来使用线程间共享的对象。

来自STL的对象被设计成不是线程安全的(因为性能问题),因此,如果用户想在两个线程之间共享std::string,用户必须用并发原语保护它的访问;

7.5. 确保线程安全的代码是递归安全的

这意味着如果您认为同一个资源可以被同一个线程使用两次,就使用递归互斥。