究竟什么是折返函数?

Laz*_*zer 190 c c++ recursion thread-safety reentrancy

大多数 时代,再进入的定义转引自维基百科:

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

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

如何安全地定义?

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

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

也,

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

在写这个问题的时候,有一件事情浮现在脑海中:像重入线程安全这样的术语是否完全绝对,即他们是否有固定的具体定义?因为,如果他们不是,这个问题不是很有意义.

pae*_*bal 184

1.如何安全定义?

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

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

没有.

例如,让我们有一个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();
}
Run Code Online (Sandbox Code Playgroud)

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

void bar()
{
    foo(nullptr);
}
Run Code Online (Sandbox Code Playgroud)

乍一看,一切似乎都没问题......但是等等:

int main()
{
    foo(bar);
    return 0;
}
Run Code Online (Sandbox Code Playgroud)

如果对互斥锁的锁定不是递归的,那么在主线程中会发生什么:

  1. main会打电话foo.
  2. foo 将获得锁定.
  3. foo会打电话bar,会打电话foo.
  4. 第二个foo将尝试获取锁定,失败并等待它被释放.
  5. 僵局.
  6. 糟糕!

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

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

你可以闻到,如果你的函数/可以访问一个可修改的持久性资源,或已/给予一个函数访问的问题气味.

(好吧,我们99%的代码应该闻到,然后......参见上一节来处理...)

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

  1. 该函数具有一个状态(即访问全局变量,甚至是一个类成员变量)
  2. 此函数可以由多个线程调用,或者可以在进程执行时在堆栈中出现两次(即函数可以直接或间接调用自身).以回调为参数的功能闻起来很多.

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

另请注意,C++方法因为可以访问而this,所以你应该研究代码以确保它们没有有趣的交互.

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

没有.

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

在单线程情况下,递归函数可以使用非重入函数(如臭名昭着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;
        }
    }
};
Run Code Online (Sandbox Code Playgroud)

第一个问题是,如果以某种方式递归调用此函数(即此函数直接或间接调用自身),代码可能会崩溃,因为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;
        }
    }
};
Run Code Online (Sandbox Code Playgroud)

这样,代码变得递归安全......但由于多线程问题,它仍然不可重入:我们必须确保使用递归互斥锁(并非所有互斥锁都是递归的)原子地进行修改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();
    }
};
Run Code Online (Sandbox Code Playgroud)

当然,这一切都假设lots of code它本身是可重入的,包括使用p.

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

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

意大利面条代码确实如此.但是,如果您正确地对代码进行分区,则可以避免重入问题.

7.1.确保所有功能都处于NO状态

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

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

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

因此,确保对象可以在堆栈中的某个点使用(即调用方法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;
    }
Run Code Online (Sandbox Code Playgroud)

如果使用检索数据的地址,即使返回const引用也可能是危险的,因为代码的其他部分可以修改它而不需要保存const引用的代码.

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

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

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

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

这意味着如果您认为同一个线程可以使用相同的资源,则使用递归互斥锁.

  • @Yttrill:我假设你在谈论第一个例子.我使用了"回调",因为从本质上讲,回调有气味.当然,递归函数会有同样的问题,但通常,人们可以很容易地分析函数及其递归性质,因此,检测它是否可重入或者是否可以递归.另一方面,回调意味着调用回调函数的作者没有关于回调做什么的任何信息,因此作者发现很难确保他/她的函数是可重入的.这就是我想表现出来的难度. (3认同)
  • 稍微狡辩一下,我实际上认为在这种情况下定义了“安全” - 这意味着该函数将仅对提供的​​变量起作用 - 即,它是其下面的定义引用的简写。重点是,这可能并不意味着其他安全理念。 (2认同)

sla*_*ker 21

"安全"的定义与常识所指的完全相同 - 它意味着"在不干扰其他事情的情况下正确地做事".您引用的六点非常清楚地表达了实现这一目标的要求.

你的3个问题的答案是3×"不".


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

没有!

例如,如果它们访问相同的全局/静态数据,则递归函数的两个同时调用可以容易地彼此搞定.


所有线程安全功能都是可重入的吗?

没有!

如果同时调用它不会出现故障,则该函数是线程安全的.但这可以通过例如使用互斥锁来阻止第二次调用的执行直到第一次完成来实现,因此一次只能执行一次调用.可重入意味着在不干扰其他调用的情况下同时执行.


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

没有!

往上看.


dra*_*ard 10

共同点:

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

如果你有这样的功能:

int add( int a , int b ) {
  return a + b;
}
Run Code Online (Sandbox Code Playgroud)

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

如果你有这样的功能:

int add_to_global( int a ) {
  return gValue += a;
}
Run Code Online (Sandbox Code Playgroud)

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

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


Ytt*_*ill 7

现在我要详细说明我以前的评论.@paercebal答案不正确.在示例代码中没有人注意到作为参数的互斥量实际上没有被传入?

我断言结论,我断言:为了使函数在并发存在的情况下是安全的,它必须是可重入的.因此,并发安全(通常是编写的线程安全)意味着可重入.

线程安全和重入都没有任何关于参数的说法:我们讨论的是函数的并发执行,如果使用了不适当的参数,这仍然是不安全的.

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

重要的是要理解一般来说,让线程安全操作包含参数是无稽之谈.如果您已完成任何数据库编程,您将理解.什么是"原子"并且可能受互斥或其他技术保护的概念必然是用户概念:在数据库上处理事务可能需要多次不间断的修改.谁可以说哪些需要与客户端程序员保持同步?

关键是"腐败"并不一定要破坏计算机上的内存并使用反序列化的写入:即使所有单独的操作都被序列化,仍然可能发生损坏.因此,当您询问函数是否是线程安全的或可重入的时,问题意味着所有适当分离的参数:使用耦合参数不构成反例.

有很多编程系统:Ocaml就是其中之一,我认为Python也有很多不可重入的代码,但它们使用全局锁来交错线程访问.这些系统不是可重入的,并且它们不是线程安全的或并发安全的,它们安全地运行只是因为它们阻止了全局并发性.

一个很好的例子是malloc.它不是可重入的,也不是线程安全的.这是因为它必须访问全局资源(堆).使用锁不会使其安全:它绝对不是可重入的.如果malloc的接口设计正确,则可以使其重入且线程安全:

malloc(heap*, size_t);
Run Code Online (Sandbox Code Playgroud)

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

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