什么是可重入函数?

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

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

  1. 不得包含静态(或全局)非常量数据。
  2. 不得将地址返回到静态(或全局)非常量数据。
  3. 只能在由调用者提供给它的数据上工作。
  4. 不能依靠锁来单身资源。
  5. 不得修改自己的代码(除非在自己的唯一线程存储中执行)
  6. 不得调用非重入式计算机程序或例程。

如何安全地定义?

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

提到的六点之间的共同点是什么,我应该记住,检查我的代码重入能力?

也,

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

在写这个问题的时候,想到的一点是:像再进入线程安全这样的术语是绝对的,它们是否有固定的具体定义? 因为,如果不是,这个问题不是很有意义。

1.如何安全地定义?

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

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

没有。

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

typedef void (*MyCallback)() ; NonRecursiveMutex mutex ; void myFunction(MyCallback f) { lock(mutex) ; f() ; unlock(mutex) ; } 

乍看之下,这个function似乎好…但是等等:

 int main(int argc, char * argv[]) { myFunction(myFunction) ; return 0 ; } 

如果互斥锁不是recursion的,那么会发生什么:

  1. main调用myFunction
  2. myFunction将获得locking
  3. myFunction将调用myFunction
  4. 第二个myFunction将尝试获取锁,失败并等待它被释放
  5. 僵局。
  6. 糟糕!

好吧,我使用Callback的东西骗了我。 但是很容易想象更复杂的代码具有相似的效果。

3.提到的六点之间的共同点是什么,我应该记住,检查我的代码的重入能力?

如果你的函数有/可以访问一个可修改的永久资源,或者有/可以访问一个有异味的函数,你可以闻到一个问题。

好吧,99%的代码应该闻起来,然后…见上一节来处理…

所以,研究你的代码,其中一点应该提醒你:

  1. 该函数有一个状态(即访问一个全局variables,甚至一个类成员variables)
  2. 这个函数可以被多个线程调用,也可以在执行过程中出现两次(即函数可以直接或间接地调用它自己)。 以callback为参数的函数闻起来很多。

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

还要注意,C ++方法因为可以访问它而闻起来 ,所以你应该研究代码以确保它们没有有趣的交互。

4.1。 所有recursion函数都是可重入的吗?

没有。

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

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

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

在上面的例子中,我展示了一个明显的线程安全函数是不是可重入的。 好吧,我因为callback参数而被欺骗了。 但是,有多种方法通过让一个线程获得两次非recursion锁来锁死线程。

4.3。 所有recursion和线程安全的函数都是可重入的吗?

如果用“recursion”表示“recursion安全”,我会说“是”。

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

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

5.再保证和线程安全这些术语是绝对的吗?它们是否有固定的具体定义?

我相信他们有,但是,然后,评估一个函数是线程安全的或可重入可能是困难的。 这就是为什么我使用上面的术语“ 气味” :你可以发现一个函数不是可重入的,但是可能很难确定一个复杂的代码是可重入的

6.一个例子

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

 struct MyStruct { P * p ; void foo() { if(this->p == NULL) { this->p = new P() ; } // Lots of code, some using this->p if(this->p != NULL) { delete this->p ; this->p = NULL ; } } } ; 

第一个问题是,如果以某种方式recursion地调用这个函数(即,这个函数直接或间接地调用它自己),那么代码可能会崩溃,因为this->p在最后一次调用结束时会被删除,在第一次通话结束之前使用。

因此,这个代码不是recursion安全的

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

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

这样,代码就变成了recursion安全的了。但是由于multithreading的问题,它仍然是不可重入的:我们必须确定cp的修改是自动完成的,使用recursion互斥体(并不是所有的互斥体都是recursion的) :

 struct MyStruct { mutex m ; // recursive mutex size_t c ; P * p ; void foo() { lock(m) ; if(c == 0) { this->p = new P() ; } ++c ; unlock(m) ; // Lots of code, some using this->p lock(m) ; --c ; if(c == 0) { delete this->p ; this->p = NULL ; } unlock(m) ; } } ; 

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

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

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

意大利面代码是相当真实的。 但是,如果你正确地分割你的代码,你将避免重入问题。

7.1。 确保所有function都没有状态。

他们只能使用参数,他们自己的局部variables,没有状态的其他函数,如果他们返回的话,它们将返回数据的副本。

7.2。 确保你的对象是“recursion安全的”。

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

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

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的对象被devise为不是线程安全的(因为性能问题),因此,如果用户想要在两个线程之间共享std::string ,则用户必须使用并发基元来保护其访问;

7.5。 确保你的线程安全的代码是recursion安全的

这意味着使用recursion互斥体,如果你相信同一个线程可以使用两次相同的资源。

“安全”的定义与常识所规定的一样,意思是“正确地做事,不干涉其他事情”。 您引用的六点非常清楚地expression了实现这一点的要求。

3个问题的答案是3ד否”。


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

没有!

例如,如果两个同时调用recursion函数的参数是相同的全局/静态数据,则可能很容易造成对方的错误。


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

没有!

如果同时调用,则函数是线程安全的。 但是这可以通过例如使用一个互斥来阻止第二次调用的执行直到第一次完成,所以一次只能调用一次。 重入意味着同时执行而不干扰其他调用


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

没有!

往上看。

共同的主题:

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

如果你有这样的function:

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

那么它不依赖于任何外部的状态。 行为是明确的。

如果你有这样的function:

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

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

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

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

我对结论持有异议,我断言:在并发的情况下,函数是安全的,它必须是可重入的。 因此,并发安全(通常是线程安全的)意味着可重入。

线程既不安全,也不可重入,对于参数没有任何意义:我们正在讨论函数的并发执行,如果使用了不合适的参数,这仍然是不安全的。

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

一般来说,线程安全操作包含参数是无稽之谈 。 如果你已经做了任何数据库编程,你会明白。 什么是“primefaces”的概念,可能受到互斥或其他技术的保护,这概念一定是用户的概念:在数据库上处理事务可能需要多次不中断的修改。 谁可以说哪些需要保持同步,但客户端程序员?

重点在于,“腐败”不需要在非序列化写入的情况下搞乱计算机上的内存:即使所有单独操作都被序列化,仍然会发生损坏。 因此,当你问一个函数是线程安全还是重入时,这个问题意味着所有适当分离的参数:使用耦合参数并不构成反例。

有很多编程系统:Ocaml是一个,我也认为Python,它们中有很多不可重入的代码,但它使用全局锁来交叉线程接口。 这些系统不是可重入的,它们不是线程安全的或并发安全的,只是因为它们阻止了全局的并发而安全地运行。

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

 malloc(heap*, size_t); 

现在它可以是安全的,因为它将序列化共享访问的责任转移到一个单一的堆到客户端。 如果存在单独的堆对象,则尤其不需要工作。 如果使用普通的堆,客户端必须序列化访问。 在函数里面使用一个锁是不够的:只要考虑一个malloclocking一个堆*,然后一个信号出现,并在同一个指针上调用malloc:死锁:信号不能继续,客户端也不能,因为它被中断。

一般来说,锁不会使线程安全……它们实际上是通过不恰当地尝试pipe理客户拥有的资源来破坏安全。 locking必须由对象制造商完成,这是知道有多less对象被创build以及将如何使用的唯一代码。

列出的要点之中的“共同线程”(双关语意思是?)是函数不得做任何会影响同一函数的任何recursion或并发调用行为的任何事情。

因此,例如静态数据是一个问题,因为它由所有线程拥有; 如果一个调用修改了一个静态variables,那么所有的线程都使用修改的数据,从而影响他们的行为 自我修改代码(尽pipe很less遇到,在某些情况下被阻止)将是一个问题,因为虽然有多个线程,但只有一个代码副本; 代码也是必不可less的静态数据。

基本上是可重入的,每个线程必须能够像使用唯一的用户那样使用该function,而如果一个线程能够以不确定的方式影响另一个线程的行为,情况就不是这样。 主要是这涉及到每个线程都有独立的或者是常量的数据。

所有这一切,点(1)不一定是真的; 例如,您可能会合法地使用一个静态variables来保留recursion计数以防止过度recursion或分析algorithm。

线程安全函数不必是可重入的; (6)表示这样的function不是可重入的,可以通过专门防止锁的重入来实现线程安全。 关于第(6)点,调用locking的线程安全函数的函数在recursion中使用并不安全(它将会死锁),因此不被称为可重入的,尽pipe它对于并发可能是安全的,在多个线程同时具有这种function的程序计数器(仅与locking的区域不同)的意义上仍然是可重入的。 可能是这有助于区分线程安全性与再入性(或者可能增加您的困惑!)。

您的“另外”问题的答案是“否”,“否”和“否”。 只是因为一个函数是recursion的和/或线程安全的,它不会使其重新进入。

这些types的函数中的每一个都可能会在您引用的所有点上失败。 (虽然我不是100%确定的第5点)。

术语“线程安全”和“重入”仅仅意味着它们的定义。 在这种情况下,“安全” 意味着你在下面引用的定义。

这里的“安全”当然并不意味着在更广泛的意义上说安全,即在给定的环境下调用给定的函数不会完全满足您的应用程序。 总而言之,一个函数可能会在您的multithreading应用程序中可靠地产生期望的效果,但是不能根据定义限定为重入或线程安全。 相反,您可以调用可重入函数,这会在multithreading应用程序中产生各种不希望的,意想不到的和/或不可预测的效果。

recursion函数可以是任何东西,而且重入式的定义比线程安全的更强,所以你的编号问题的答案都不是。

读取重入的定义,人们可以将其归纳为一个函数,它不会修改任何超出所谓的修改范围的内容。 但是,你不应该只依靠总结。

在一般情况下,multithreading编程是非常困难的。 知道代码重入的哪一部分只是这个挑战的一部分。 线程安全性不是附加的。 与使用重入函数拼接在一起,最好使用总体线程安全 devise模式,并使用此模式来指导您使用程序中的每个线程和共享资源。