C语言 可重入函数到底是什么?

yqyhoc1h  于 2023-06-21  发布在  其他
关注(0)|答案(8)|浏览(101)

大多数时候,再入的定义是从Wikipedia引用的:
一个计算机程序或例程被描述为可重入的,如果它可以在其前一次调用完成之前被安全地再次调用(即它可以安全地并发执行)。可重入,计算机程序或例行程序:
1.必须不包含静态(或全局)非常量数据。
1.不能将地址返回到静态(或全局)非常量数据。
1.必须只处理调用方提供给它的数据。
1.不能依赖于单例资源的锁。
1.不得修改自己的代码(除非在自己唯一的线程存储中执行)
1.不能调用不可重入的计算机程序或例程。

*安全是如何定义的?

如果一个程序可以安全地并发执行,是否就意味着它是可重入的?
在检查代码的可重入能力时,我应该记住上述六点之间的共同点到底是什么?
还有
1.所有递归函数都是可重入的吗?
1.所有的线程安全函数都是可重入的吗?
1.所有的递归和线程安全函数都是可重入的吗?
在写这个问题的时候,我想到了一件事:像reentrancethread safety这样的术语是绝对的吗?是否有固定的具体定义?因为,如果他们不是,这个问题就没有多大意义了。

jfewjypa

jfewjypa1#

1. safely如何定义?

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

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

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

void bar()
{
    foo(nullptr);
}

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

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

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

  1. main将调用foo
  2. foo将获取锁。
  3. foo将调用barbar将调用foo
    1.第二个foo将尝试获取锁,失败并等待它被释放。
    1.僵局。
    1.哎呀...
    好吧,我作弊了,使用回调的东西。但是很容易想象更复杂的代码片段具有类似的效果。

3.在检查代码的可重入能力时,上述六点之间的共同点是什么?

如果你的函数可以访问一个可修改的持久资源,或者可以访问一个smells的函数,你可以smell一个问题。
Ok, 99% of our code should smell, then… See last section to handle that…
所以,研究你的代码,其中一点应该提醒你:
1.该函数具有一个状态(即访问全局变量,甚至是数据成员)
1.此函数可以由多个线程调用,或者在进程执行时在堆栈中出现两次(即函数可以直接或间接地调用自身)。函数以回调函数作为参数smell很多。
请注意,不可重入性是病毒性的:可以调用可能的不可重入函数的函数不能被认为是可重入的。
还要注意的是,C++方法smell是因为它们可以访问this,所以您应该研究代码以确保它们没有有趣的交互。

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

没有
在多线程情况下,访问共享资源的递归函数可能会被多个线程同时调用,从而导致数据损坏。
在单线程情况下,递归函数可以使用不可重入函数(如臭名昭著的strtok),或者使用全局数据而不处理数据已经在使用的事实。所以你的函数是递归的,因为它直接或间接地调用自己,但它仍然可以是recursive-unsafe

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

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

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

如果你说的“递归”是指“递归安全”,我会说“是”。
如果你可以保证一个函数可以被多个线程同时调用,并且可以直接或间接地调用它自己,而不会有任何问题,那么它就是可重入的。
问题是评估这个保证。。^_^

5.重入、线程安全这些术语是绝对的吗?是否有固定的具体定义?

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

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将在最后一次调用结束时被删除,并且仍然可能在第一次调用结束前被使用。
因此,此代码不是recursive-safe
我们可以使用引用计数器来纠正这个问题:

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的修改将使用recursivemutex(并非所有mutex都是递归的)以原子方式完成:

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

当然,这一切都假设lots of code本身是可重入的,包括使用p
上面的代码甚至不是远程exception-safe,但这是另一个故事...^_^

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

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

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

因此,用户负责使用互斥体来使用线程之间共享的对象。
STL中的对象被设计为非线程安全的(由于性能问题),因此,如果用户希望在两个线程之间共享std::string,则用户必须使用并发原语来保护其访问;

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

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

rslzwgfq

rslzwgfq2#

“安全”的定义完全符合常识-它意味着“正确地做自己的事情而不干扰其他事情”。你提到的六点非常清楚地表达了实现这一目标的要求。
你的三个问题的答案是3ד否”。

  • 所有递归函数都是可重入的吗?*
    不!

例如,如果两个递归函数同时访问相同的全局/静态数据,那么它们很容易相互干扰。

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

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

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

见上文。

rjjhvcjd

rjjhvcjd3#

共同点:
如果例程在被中断时被调用,那么行为是否被很好地定义?
如果你有一个这样的函数:

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

它不依赖于任何外部状态。行为定义明确。
如果你有一个这样的函数:

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

结果在多线程上没有很好地定义。如果时机不对,信息可能会丢失。
可重入函数的最简单形式是专门对传递的参数和常量值进行操作。其他任何东西都需要特殊处理,或者通常不是可重入的。当然,参数不能引用可变的全局变量。

0ve6wy6x

0ve6wy6x4#

现在我必须详细说明我先前的评论。@paercebal的答案是不正确的。在示例代码中,没有人注意到应该是参数的互斥量实际上没有传入吗?
我不同意这个结论,我Assert:对于在存在并发的情况下安全的函数,它必须是可重入的。因此,并发安全(通常写为线程安全)意味着可重入。
线程安全和可重入都没有什么要说的参数:我们讨论的是函数的并发执行,如果使用了不适当的参数,它仍然是不安全的。
例如,memcpy()是线程安全和可重入的(通常)。显然,如果从两个不同的线程调用指向相同目标的指针,它将无法正常工作。这就是SGI定义的要点,将责任放在客户端上,以确保客户端同步对相同数据结构的访问。
重要的是要理解,一般来说,让线程安全操作包含参数是无意义的。如果你做过任何数据库编程,你就会明白。什么是“原子”的概念,并且可能受到互斥或其他技术的保护,这必然是用户的概念:在数据库上处理事务可能需要多次不间断的修改。除了客户端程序员,谁能说哪些需要保持同步呢?
关键是,“腐败”并不一定要用未序列化的写入来扰乱计算机上的内存:即使所有单独的操作都是串行的,仍然可能发生讹误。因此,当你问一个函数是否是线程安全的或可重入的时,这个问题意味着对于所有适当分隔的参数:使用耦合的论点并不构成反例。
有很多编程系统:Ocaml是其中之一,我认为Python也是如此,它有很多不可重入的代码,但它使用全局锁来交错线程访问。这些系统不是可重入的,它们不是线程安全或并发安全的,它们安全地运行只是因为它们阻止了全局并发。
一个很好的例子是malloc。它不是可重入的,也不是线程安全的。这是因为它必须访问全局资源(堆)。使用锁并不安全:它绝对不是可重入的。如果malloc的接口设计得当,就有可能使其可重入和线程安全:

malloc(heap*, size_t);

现在它可以是安全的,因为它将序列化对单个堆的共享访问的责任转移给了客户端。特别是,如果存在单独的堆对象,则不需要任何工作。如果使用公共堆,则客户端必须序列化访问。在函数中使用锁**是不够的:假设一个malloc锁定了一个heap*,然后一个信号沿着并在同一个指针上调用malloc:死锁:信号不能继续,客户端也不能,因为它被中断了。
一般来说,锁不会使事情线程安全。它们实际上通过不适当地试图管理客户机所拥有的资源而破坏安全性。锁定必须由对象制造商完成,这是唯一知道创建了多少对象以及如何使用它们的代码。

vwkv1x7d

vwkv1x7d5#

“共同的线索”(双关语!?)列出的要点之一是函数不能做任何会影响对同一函数的任何递归或并发调用的行为的事情。
例如,静态数据是一个问题,因为它由所有线程拥有;如果一个调用修改静态变量,则所有线程使用修改的数据,从而影响它们的行为。自我修改代码(虽然很少遇到,在某些情况下被阻止)将是一个问题,因为虽然有多个线程,但只有一个代码副本;代码也是必不可少的静态数据。
从本质上讲,为了可重入,每个线程都必须能够像唯一用户一样使用函数,如果一个线程可以以非确定性方式影响另一个线程的行为,情况就不是这样了。这主要涉及每个线程都有函数处理的单独或恒定数据。
综上所述,第(1)点不一定是正确的;例如,您可以合法地并通过设计使用静态变量来保留递归计数,以防止过度递归或分析算法。
线程安全函数不需要是可重入的;它可以通过专门使用锁来防止可重入性来实现线程安全性,并且点(6)表明这样的函数是不可重入的。关于第(6)点,调用线程安全函数的函数在递归中使用是不安全的(它将死锁),因此不被称为可重入的,尽管它可能对并发性是安全的,并且在多个线程可以同时在这样的函数中拥有它们的程序计数器的意义上仍然是可重入的(只是不具有锁定区域)。这可能有助于区分线程安全性和线程延迟性(或者可能会增加您的困惑!).

92vpleto

92vpleto6#

你的“另外”问题的答案是“否”、“否”和“否”。仅仅因为一个函数是递归的和/或线程安全的,它并不意味着它是可重入的。
这些类型的函数中的每一个都可能在您引用的所有点上失败。(虽然我不是100%确定第5点)。

yftpprvb

yftpprvb7#

  • 不可重入函数意味着将有一个静态上下文,由函数维护。当你第一次进入时,会有一个新的上下文为你创建。而下一次进入时,为了方便令牌分析,你不需要发送更多的参数。例如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;
}
  • 与不可重入相反,可重入函数意味着在任何时候调用函数都会得到相同的结果,而不会产生副作用。因为没有上下文。
  • 从线程安全的Angular 看,它只是意味着在当前时间,在当前进程中,对公共变量只有一次修改。因此您应该添加锁保护,以确保一次只对公共字段进行一次更改。
  • 所以线程安全和可重入是两个不同观点。2可重入函数安全说你应该在下次上下文分析之前清除上下文。线程安全说你应该保持访问公共字段的顺序。
p1iqtdky

p1iqtdky8#

术语“线程安全”和“可重入”的含义仅与它们的定义完全相同。“安全”在这种情况下意味着 * 只有 * 你下面引用的定义说。
这里的“安全”当然不是指在更广泛的意义上的安全,即在给定的上下文中调用给定的函数不会完全破坏应用程序。总之,一个函数可能在多线程应用程序中可靠地产生所需的效果,但根据定义,它既不是可重入的,也不是线程安全的。相反,您可以以在多线程应用程序中产生各种不希望的、意外的和/或不可预测的效果的方式调用可重入函数。
递归函数可以是任何东西,而可重入的定义比线程安全的定义更强,所以你的问题的答案都是否定的。
阅读可重入的定义,人们可能会将其总结为一个函数,它不会修改任何超出您所称的修改范围的任何内容。但你不应该只依赖摘要。
多线程编程在一般情况下就是extremely difficult。知道代码的哪一部分可重入只是这个挑战的一部分。线程安全性不是附加的。与其试图拼凑可重入函数,不如使用一个整体的thread-safedesign pattern,并使用此模式来指导您在程序中使用 * every * 线程和共享资源。

相关问题