What exactly is a reentrant function?
大多数情况下,重新进入的定义引用自维基百科:
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。如何安全定义?
语义上的。在这种情况下,这不是一个很难定义的术语。它的意思是"你可以做到,没有风险"。好的。2。如果一个程序可以同时安全地执行,它是否总是意味着它是可重入的?
不。好的。
例如,让我们有一个C++函数,它既把锁和回调作为参数:好的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | #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(); } |
另一个函数很可能需要锁定相同的互斥体:好的。
1 2 3 4 | void bar() { foo(nullptr); } |
乍一看,一切似乎都很好……但等一下:好的。
1 2 3 4 5 | int main() { foo(bar); return 0; } |
如果互斥锁不是递归的,那么在主线程中将发生以下情况:好的。
好吧,我用回拨器作弊了。但很容易想象,更复杂的代码片段也会产生类似的效果。好的。三。在检查代码的可重入能力时,我应该记住的六点之间的公共线程究竟是什么?
如果函数具有/授予对可修改持久资源的访问权限,或者具有/授予对气味函数的访问权限,则可能会闻到问题的味道。好的。
(好的,99%的代码应该有气味,然后……请参阅最后一节处理这个…)好的。
因此,研究你的代码,其中一点应该提醒你:好的。
注意,不可重入性是病毒性的:一个可以调用可能的不可重入函数的函数不能被认为是可重入的。好的。
也请注意,C++方法之所以有味道,是因为它们可以访问EDCOX1×7,所以你应该研究代码以确保它们没有有趣的交互。好的。4.1。所有递归函数都是可重入的吗?
不。好的。
在多线程情况下,多个线程可以同时调用访问共享资源的递归函数,从而导致数据错误/损坏。好的。
在单线程情况下,递归函数可以使用不可重入函数(如臭名昭著的
在上面的示例中,我演示了一个明显的threadsafe函数是如何不可重入的。好吧,我作弊是因为回调参数。但是,有多种方法可以通过让线程获取两次非递归锁来死锁线程。好的。4.3。所有递归和线程安全函数都是可重入的吗?
如果"递归"的意思是"递归安全",我会说"是"。好的。
如果您可以保证一个函数可以被多个线程同时调用,并且可以直接或间接地调用它自己,而不会出现问题,那么它是可重入的。好的。
问题是评估这一保证……^^好的。5。重入和线程安全等术语是绝对的吗?也就是说,它们有固定的具体定义吗?
我相信它们已经有了,但是评估一个函数是线程安全的,或者是可重入的可能很困难。这就是我使用上面这个术语的原因:您可以发现一个函数是不可重入的,但是很难确定一段复杂的代码是可重入的。好的。6。一个例子
假设您有一个对象,其中一个方法需要使用资源:好的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | 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; } } }; |
第一个问题是,如果以某种方式递归调用此函数(即,此函数直接或间接调用自身),代码可能会崩溃,因为
因此,此代码不是递归安全的。好的。
我们可以使用参考计数器来更正:好的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | 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; } } }; |
这样,代码就变得递归安全了……但由于多线程问题,它仍然是不可重入的:我们必须确保使用递归互斥体(并非所有互斥体都是递归的)原子地修改
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | #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(); } }; |
当然,这一切都假定
上面的代码甚至不是远程异常安全的,但这是另一个故事…^^好的。7。嘿,99%的代码是不可重入的!
意大利面代码是完全正确的。但是,如果正确地划分代码,就可以避免重入问题。好的。7.1。确保所有函数都没有状态
它们必须只使用参数、它们自己的局部变量、其他没有状态的函数,如果返回数据,则返回数据的副本。好的。7.2。确保对象是"递归安全的"
对象方法可以访问
因此,确保对象可以在堆栈中的一个点(即调用方法A)使用,然后在另一个点(即调用方法B)使用,而不会损坏整个对象。设计对象以确保在退出方法时,对象是稳定和正确的(没有悬空指针、没有矛盾的成员变量等)。好的。7.3。确保正确封装所有对象
其他人不应访问其内部数据:好的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | // 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; } |
即使返回常量引用,如果使用检索数据的地址,也可能是危险的,因为代码的其他部分可能会修改它,而不通知保存常量引用的代码。好的。7.4。确保用户知道您的对象不是线程安全的
因此,用户负责使用互斥体来使用线程之间共享的对象。好的。
STL中的对象被设计为不具有线程安全性(由于性能问题),因此,如果用户希望在两个线程之间共享
这意味着如果您相信同一个资源可以被同一线程使用两次,那么就使用递归互斥体。好的。好啊。
"安全"的定义正是常识所规定的——它的意思是"在不干扰其他事物的情况下正确地做自己的事情"。你提到的六点非常清楚地表达了实现这一点的要求。
你的3个问题的答案是3×"不"。
所有递归函数都是可重入的吗?
不!
如果例如,它们访问相同的全局/静态数据。
所有线程安全函数都是可重入的吗?
不!
如果函数在并发调用时不发生故障,那么它是线程安全的。但这可以实现,例如,通过使用互斥锁来阻止第二次调用的执行,直到第一次完成,因此一次只能进行一次调用。重入意味着在不干扰其他调用的情况下并发执行。
所有递归和线程安全函数都是可重入的吗?
不!
见上文。
普通螺纹:
如果在例程中断时调用它,那么该行为是否定义良好?
如果你有这样的功能:
1 2 3 | int add( int a , int b ) { return a + b; } |
那么它就不依赖于任何外部状态。这种行为是很明确的。
如果你有这样的功能:
1 2 3 | int add_to_global( int a ) { return gValue += a; } |
在多个线程上没有很好地定义结果。如果时机不对,信息可能会丢失。
可重入函数的最简单形式是只对传递的参数和常量值进行操作。其他任何东西都需要特殊的处理,或者通常是不可重入的。当然,参数不能引用可变的全局变量。
现在我必须详细阐述我以前的评论。@Paercebal答案不正确。在这个示例代码中,没有人注意到应该是参数的互斥体实际上没有传入?
我断言,我对这个结论表示怀疑:为了在并发性存在的情况下保证函数的安全性,它必须是可重入的。因此,并发安全(通常是书面线程安全)意味着可重入。
线程安全的和可重入的都没有关于参数的任何内容:我们讨论的是函数的并发执行,如果使用不适当的参数,这仍然是不安全的。
例如,memcpy()是线程安全的,并且是可重入的(通常)。显然,如果使用来自两个不同线程的指向同一目标的指针调用,它将无法按预期工作。这就是SGI定义的重点,将责任放在客户机上,以确保客户机同步访问同一数据结构。
重要的是要理解,一般来说,让线程安全操作包含参数是无稽之谈。如果你已经做过任何数据库编程,你会理解的。什么是"原子"的概念,可能受互斥或其他技术保护,这必然是一个用户概念:在数据库上处理事务可能需要多次不间断的修改。除了客户端程序员,谁能说哪些需要保持同步?
关键是"损坏"不必用未经序列化的写入来破坏计算机上的内存:即使所有单个操作都进行了序列化,也可能发生损坏。因此,当您询问一个函数是线程安全的还是可重入的时,这个问题意味着所有适当分离的参数:使用耦合参数并不构成反例。
有很多编程系统:ocaml是一个,我认为python也是一个,里面有很多不可重入的代码,但是它使用一个全局锁来交错线程访问。这些系统不是可重入的,也不是线程安全的或并发安全的,它们的安全运行仅仅是因为它们阻止了全局并发。
马尔洛克就是一个很好的例子。它不是可重入的,也不是线程安全的。这是因为它必须访问全局资源(堆)。使用锁并不能保证安全:它绝对不能再进入。如果malloc的接口设计得当,就有可能使其重新进入并保证线程安全:
1 | malloc(heap*, size_t); |
现在它可以是安全的,因为它将对单个堆的共享访问进行串行化的责任转移给客户机。特别是,如果有单独的堆对象,则不需要进行任何工作。如果使用公共堆,则客户端必须对访问进行序列化。在函数内部使用一个锁是不够的:只要考虑一个malloc锁定一个堆*,然后一个信号出现并在同一个指针上调用malloc:deadlock:信号不能继续,并且客户端也不能继续,因为它被中断了。
一般来说,锁不能保证线程安全。他们实际上通过不适当地管理客户拥有的资源来破坏安全。锁定必须由对象制造商完成,这是唯一知道创建了多少对象以及如何使用这些对象的代码。
"普通线"(双关语!?)在列出的点中,函数不能做任何会影响对同一函数的任何递归或并发调用行为的事情。
例如,静态数据是一个问题,因为它由所有线程拥有;如果一个调用修改了一个静态变量,则所有线程都使用修改后的数据,从而影响它们的行为。自我修改代码(虽然很少遇到,而且在某些情况下是被阻止的)将是一个问题,因为尽管有多个线程,但代码只有一个副本;代码也是基本的静态数据。
本质上,为了实现可重入性,每个线程必须能够像它是唯一的用户一样使用该函数,而如果一个线程可以以非确定性的方式影响另一个线程的行为,则情况并非如此。主要是指每个线程都有函数所处理的独立或常量数据。
尽管如此,点(1)并不一定是正确的;例如,您可以合法地使用静态变量来保留递归计数,以防止过度递归或分析算法。
线程安全函数不必是可重入的;它可以通过使用锁专门防止重入来实现线程安全,第(6)点表示这样的函数是不可重入的。关于点(6),一个调用线程安全函数的函数,该函数的锁在递归中使用是不安全的(它将死锁),因此不被称为可重入函数,尽管它在并发性方面可能是安全的,并且在多个线程可以同时在这样一个函数中拥有其程序计数器的意义上仍然是可重入的。狡猾(只是不与锁定区域)。这可能有助于区分线程安全性和可重入性(或者增加您的困惑!).
术语"线程安全"和"可重入者"仅指其定义所说的内容。在这种情况下,"安全"只意味着你在下面引用的定义。
这里的"safe"当然并不意味着在更广泛的意义上的safe,即在给定的上下文中调用给定的函数不会完全破坏您的应用程序。总之,一个函数可能会在多线程应用程序中可靠地产生期望的效果,但根据定义,它不符合可重入或线程安全的条件。相反,您可以以在多线程应用程序中产生各种不希望的、意外的和/或不可预测的效果的方式调用可重入函数。
递归函数可以是任何东西,并且可重入函数的定义比线程安全性强,因此对编号问题的答案都是否定的。
阅读再入者的定义,我们可以将其概括为一个函数,它不会修改任何超出您所称的修改范围的内容。但你不应该只依靠总结。
在一般情况下,多线程编程非常困难。知道代码的哪一部分是可重入的,这只是这个挑战的一部分。螺纹安全不是添加剂。与其尝试将可重入函数拼凑在一起,不如使用一个完整的线程安全设计模式,并使用这个模式来指导您使用程序中的每个线程和共享资源。
你的"同样"问题的答案是"不"、"不"和"不"。仅仅因为函数是递归的和/或线程安全的,它不会使它重新进入。
每种类型的函数都可能在您引用的所有点上失败。(尽管我不是100%确定第5点)。