什么是可重入函数?

时间:2023-03-07
本文介绍了什么是可重入函数?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着跟版网的小编来一起学习吧!

问题描述

大多数 of 次,定义重入引用自维基百科:

Most of the times, the definition of reentrance is quoted from Wikipedia:

计算机程序或例程是如果可以,则描述为可重入的安全在它之前再次调用之前的调用已经完成(即它可以安全地执行同时).为了可重入,一个计算机程序或例程:

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:

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

如何定义安全?

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

If a program can be safely executed concurrently, does it always mean that it is reentrant?

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

What exactly is the common thread between the six points mentioned that I should keep in mind while checking my code for reentrant capabilities?

还有,

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


在写这个问题的时候,想到一件事:重入线程安全之类的术语是否是绝对的,即它们是否有固定的具体定义?因为,如果不是,这个问题就没有太大意义.


While writing this question, one thing comes to mind: Are the terms like reentrance and thread safety absolute at all i.e. do they have fixed concrete definitions? For, if they are not, this question is not very meaningful.

推荐答案

1.安全是如何定义的?

语义上.在这种情况下,这不是一个硬定义的术语.这只是意味着你可以做到,没有风险".

1. How is safely defined?

Semantically. In this case, this is not a hard-defined term. It just mean "You can do that, without risk".

没有

例如,让我们有一个 C++ 函数,它将锁和回调作为参数:

For example, let's have a C++ function that takes both a lock, and a callback as a parameter:

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

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

Another function could well need to lock the same mutex:

void bar()
{
    foo(nullptr);
}

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

At first sight, everything seems ok… But wait:

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

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

If the lock on mutex is not recursive, then here's what will happen, in the main thread:

  1. main 将调用 foo.
  2. foo 将获取锁.
  3. foo 会调用 bar,后者会调用 foo.
  4. 第二个 foo 将尝试获取锁,失败并等待它被释放.
  5. 僵局.
  6. 糟糕……
  1. main will call foo.
  2. foo will acquire the lock.
  3. foo will call bar, which will call foo.
  4. the 2nd foo will try to acquire the lock, fail and wait for it to be released.
  5. Deadlock.
  6. Oops…

好吧,我欺骗了,使用回调的东西.但是很容易想象具有类似效果的更复杂的代码段.

Ok, I cheated, using the callback thing. But it's easy to imagine more complex pieces of code having a similar effect.

如果您的函数具有/授予访问可修改持久资源的权限,或者具有/授予访问异味的功能,则您可以嗅到问题.

You can smell a problem if your function has/gives access to a modifiable persistent resource, or has/gives access to a function that smells.

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

因此,在研究您的代码时,其中一点应该提醒您:

So, studying your code, one of those points should alert you:

  1. 函数有状态(即访问全局变量,甚至类成员变量)
  2. 这个函数可以被多个线程调用,也可以在进程执行时在堆栈中出现两次(即函数可以直接或间接调用自身).函数将回调作为参数气味很多.

请注意,不可重入是病毒式的:可以调用可能的不可重入函数的函数不能被视为可重入.

Note that non-reentrancy is viral : A function that could call a possible non-reentrant function cannot be considered reentrant.

还要注意,C++ 方法气味,因为它们可以访问this,因此您应该研究代码以确保它们没有有趣的交互.

Note, too, that C++ methods smell because they have access to this, so you should study the code to be sure they have no funny interaction.

没有

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

In multithreaded cases, a recursive function accessing a shared resource could be called by multiple threads at the same moment, resulting in bad/corrupted data.

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

In singlethreaded cases, a recursive function could use a non-reentrant function (like the infamous strtok), or use global data without handling the fact the data is already in use. So your function is recursive because it calls itself directly or indirectly, but it can still be recursive-unsafe.

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

In the example above, I showed how an apparently threadsafe function was not reentrant. OK, I cheated because of the callback parameter. But then, there are multiple ways to deadlock a thread by having it acquire twice a non-recursive lock.

如果递归"是指递归安全",我会说是".

I would say "yes" if by "recursive" you mean "recursive-safe".

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

If you can guarantee that a function can be called simultaneously by multiple threads, and can call itself, directly or indirectly, without problems, then it is reentrant.

问题是评估这个保证……^_^

The problem is evaluating this guarantee… ^_^

我相信他们会这样做,但是,评估一个函数是线程安全的还是可重入的可能很困难.这就是我使用上面气味这个词的原因:你可以发现一个函数是不可重入的,但很难确定一段复杂的代码是可重入的

I believe they do, but then, evaluating a function is thread-safe or reentrant can be difficult. This is why I used the term smell above: You can find a function is not reentrant, but it could be difficult to be sure a complex piece of code is reentrant

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

Let's say you have an object, with one method that needs to use a resource:

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 将在最后一次调用结束,并且可能在第一次调用结束之前使用.

The first problem is that if somehow this function is called recursively (i.e. this function calls itself, directly or indirectly), the code will probably crash, because this->p will be deleted at the end of the last call, and still probably be used before the end of the first call.

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

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

We could use a reference counter to correct this:

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

这样,代码就变得递归安全了……但由于多线程问题,它仍然不可重入:我们必须确保 cp 的修改将使用 递归 互斥体(并非所有互斥体都是递归的)以原子方式完成:

This way, the code becomes recursive-safe… But it is still not reentrant because of multithreading issues: We must be sure the modifications of c and of p will be done atomically, using a recursive mutex (not all mutexes are recursive):

#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的使用.

And of course, this all assumes the lots of code is itself reentrant, including the use of p.

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

And the code above is not even remotely exception-safe, but this is another story… ^_^

意大利面条式代码确实如此.但是如果你正确地划分你的代码,你将避免重入问题.

It is quite true for spaghetti code. But if you partition correctly your code, you will avoid reentrancy problems.

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

They must only use the parameters, their own local variables, other functions without state, and return copies of the data if they return at all.

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

An object method has access to this, so it shares a state with all the methods of the same instance of the object.

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

So, make sure the object can be used at one point in the stack (i.e. calling method A), and then, at another point (i.e. calling method B), without corrupting the whole object. Design your object to make sure that upon exiting a method, the object is stable and correct (no dangling pointers, no contradicting member variables, etc.).

其他人不应访问其内部数据:

No one else should have access to their internal data:

    // 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 引用的代码.

Even returning a const reference could be dangerous if the user retrieves the address of the data, as some other portion of the code could modify it without the code holding the const reference being told.

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

Thus, the user is responsible to use mutexes to use an object shared between threads.

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

The objects from the STL are designed to be not thread-safe (because of performance issues), and thus, if a user want to share a std::string between two threads, the user must protect its access with concurrency primitives;

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

This means using recursive mutexes if you believe the same resource can be used twice by the same thread.

这篇关于什么是可重入函数?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持跟版网!

上一篇:C++中的尾递归 下一篇:在 C++ 中递归到 main() 是否合法?

相关文章