今日免费次数已满, 请开通会员/明日再来

6

我有一个使用堆内存的函数,如果在同一个函数的另一个实例完成之前调用它,那么它将出现严重错误。 如何在编译时防止这种情况发生?


28
避免写递归调用有多难?这不是很容易吗? - anon
10
问题的标题似乎有误导性......你并不打算让这个函数不递归调用,而是希望它能够同时并发地运行。如果我理解问题中的文本正确的话,请修改问题的标题。 - David Rodríguez - dribeas
10
为了更加清晰:在您的上下文中,您谈论的是可重入性,而不是递归。 - Igor Korkhov
2
@Neil:假设您调用了某个第三方模块,该模块理论上可以调用您的函数,而无需您显式进行递归调用,因此您的函数将被递归调用。 - falstro
5
(假设你在某个回调场景中)解决方案是记录函数不得从回调中调用的事实。如果用户这样做了,那就是他们的问题。你无法防止所有可能的错误用例,而且试图这样做根本不值得。 - anon
显示剩余17条评论
12个回答

9
检测任意确定性递归在编译时会非常困难。一些静态代码分析工具可能能够做到这一点,但即使如此,您也可能会遇到涉及线程的运行时情况,这些代码分析器无法检测到。
您需要在运行时检测递归。从根本上讲,这很简单:
bool MyFnSimple()
{
    static bool entered = false;
    if( entered )
    {
        cout << "Re-entered function!" << endl;
        return false;
    }
    entered = true;

    // ...

    entered = false;
    return true;
}

这当然存在最大的问题,就是它不是线程安全的。有几种方法可以使其线程安全,其中最简单的方法是使用关键部分并阻止第二个进入,直到第一个已离开。 Windows代码(未包括错误处理):
bool MyFnCritSecBlocking()
{
    static HANDLE cs = CreateMutex(0, 0, 0);
    WaitForSingleObject(cs, INFINITE);
    // ... do stuff
    ReleaseMutex(cs);
    return true;
}

如果您希望当函数被重新进入时返回错误,您可以在获取它之前先测试critsec:
bool MyFnCritSecNonBlocking()
{
    static HANDLE cs = CreateMutex(0, 0, 0);
    DWORD ret = WaitForSingleObject(cs, 0);
    if( WAIT_TIMEOUT == ret )
        return false;   // someone's already in here
    // ... do stuff
    ReleaseMutex(cs);
    return true;
}

除了使用静态布尔值和critsecs,可能有无限种方法来解决这个问题。其中一个方法是将本地值与Windows中的Interlocked函数之一进行测试的组合:

bool MyFnInterlocked()
{
    static LONG volatile entered = 0;
    LONG ret = InterlockedCompareExchange(&entered, 1, 0);
    if( ret == 1 )
        return false;   // someone's already in here
    // ... do stuff
    InterlockedExchange(&entered, 0);
    return false;
}

当然,您必须考虑异常安全性和死锁。您不希望函数中的失败使其无法被任何代码进入。您可以在上述任何结构中包装RAII,以确保在函数中发生异常或提前退出时释放锁定。

更新:

阅读评论后,我意识到我可以包含一个演示如何实现RAII解决方案的代码,因为您编写的任何实际代码都将使用RAII来处理错误。这里是一个简单的RAII实现,还说明了运行时出现问题时会发生什么:
#include <windows.h>
#include <cstdlib>
#include <stdexcept>
#include <iostream>

class CritSecLock
{
public:
    CritSecLock(HANDLE cs) : cs_(cs)
    {
        DWORD ret = WaitForSingleObject(cs_, INFINITE);
        if( ret != WAIT_OBJECT_0 ) 
            throw std::runtime_error("Unable To Acquire Mutex");
        std::cout << "Locked" << std::endl;
    }
    ~CritSecLock()
    {
        std::cout << "Unlocked" << std::endl;
        ReleaseMutex(cs_);
    }
private:
    HANDLE cs_;
};

bool MyFnPrimitiveRAII()
{
    static HANDLE cs = CreateMutex(0, 0, 0);
    try
    {
        CritSecLock lock(cs);
        // ... do stuff
        throw std::runtime_error("kerflewy!");
        return true;
    }
    catch(...)
    {
        // something went wrong 
        // either with the CritSecLock instantiation
        // or with the 'do stuff' code
        std::cout << "ErrorDetected" << std::endl;
        return false;
    }
}

int main()
{
    MyFnPrimitiveRAII();
    return 0;
}

3
我觉得你提到 RAII 仅仅是附带一提,而且在代码中并没有出现,这几乎让我感到震惊。 - Matthieu M.
1
@Matthieu:我试图说明检测重入背后的原则,而不是RAII。您不需要RAII来实现检测重入的解决方案,尽管在实际代码中,您会像更严格地检查返回值等一样使用RAII或其他构造来处理错误和异常。但是一次只讲一个原则。我还可以指出,即使我是唯一一个提到RAII的人,您似乎也不介意其他人的代码中没有它。 - John Dibling
也就是说,RAII 不能实现我展示的检测重入的习惯用法。 - John Dibling
@John:我确实在意了,但是你提到它却没有应用它,感觉有点奇怪。每当我遇到问题时,我通常都很期待能看到你的答案,这次有点失望了 :) - Matthieu M.
@Matthieu:在重新阅读这篇文章并看到你的评论后,第二天我意识到你是正确的。我应该添加代码来说明如何实现RAII。我的帖子自然而然地引导到那里。已编辑。干杯。 - John Dibling

9
你的问题不太清楚,你是指单线程场景(递归或相互递归)还是多线程场景(可重入性)?
在多线程场景下,编译器无法在编译时防止这种情况发生,因为编译器对线程没有了解。
在单线程场景下,除了用脑子分析控制流并证明函数不会调用自身以及它调用的函数也不会回头调用它之外,我不知道有什么方法可以在编译时防止递归调用。只要你能分析控制流并证明函数不会调用自身,而且它调用的所有函数也不会回头调用它,那么就应该是安全的。

3
+1 for the brain!“在编译时只有一个解决方案-思考”。 - Johann Gerell

7

如果没有进行静态分析,你无法在编译时完成这项操作。以下是一个安全的递归断言:

#include <cassert>

class simple_lock
{
public:
    simple_lock(bool& pLock):
    mLock(pLock)
    {
        assert(!mLock && "recursive call");
        mLock = true;
    }

    ~simple_lock(void)
    {
        mLock = false;
    }

private:
    simple_lock(const simple_lock&);
    simple_lock& operator=(const simple_lock&);

    bool& mLock;
};

#define ASSERT_RECURSION static bool _lockFlag = false; \
                            simple_lock _lock(_lockFlag)

void foo(void)
{
    ASSERT_RECURSION;

    foo();
}

int main(void)
{
    foo();
    //foo();
}

5

如果没有某种静态分析器,您无法在编译时完成此操作。但是,可以进行简单的运行时检查:

注意:为了防止多线程并发但非递归调用,您需要更加健壮的解决方案。

void myFunc() {
  static int locked = 0;
  if (locked++)
  {
    printf("recursion detected\n!");
  }

  ....

  locked--;
}

注意:您应该将此函数放在一个.c.cc文件中,而不是头文件中。
如果您有多线程,请使用pthread锁来控制对其引用的共享变量的访问。

+1. @j coe,无法在编译时防止函数本身的调用。请按照此答案中所述,在函数本身中编写防御性代码。 - Daniel Daranas
@Daniel 在这种情况下,正确的答案是“你不能这样做”。 - anon

4

在任何图灵完备的语言中,这个问题是不可判定的。虽然我无法证明它,但我知道。


9
需要翻译的内容:想要证明吗?如果它是可判定的,那么你可以编写一个函数void f() {if (!recurses(f)) f();},仅当它不会递归时才递归。 - Mike Seymour
很好。就像停机证明一样。 - Chris H

2

c++-faq-lite在类似情况下提供了一些不错的建议:写下你预期在处理这样的事情时会出现问题的注释:

// We'll fire you if you try recursion here

我还没有查看建议是否也适用于递归


2
你最好使用互斥锁。你可以使用信号量,但我个人更喜欢使用互斥锁。这将允许你防止其他线程调用它。
*编辑*
啊,你希望它在编译时发生?
那么你走了一条不归路,我的朋友。

有点打错字了。很好发现。我本来想说“你不希望它在运行时发生吗?”,但由于某种原因我的手指打出了“编译”。 - ChrisBD

2

函数调用堆栈是在运行时创建的,编译时只能检查函数本身是否递归,即它是否调用自己?


1

这个可以编译

#include <stdio.h>

int testFunc() {
#define testFunc
printf("Ok\n");
}
#undef testFunc
int main() { testFunc(); }

这不是

#include <stdio.h>

int testFunc() {
#define testFunc
printf("Ok\n");
testFunc();
}
#undef testFunc
int main() { testFunc(); }

错误:test.c:7: error: expected expression before ‘)’ token 它也适用于多功能递归:
#include <stdio.h>

int testFunc1() {
#define testFunc1
printf("1\n");
testFunc2();
}

int testFunc2() {
#define testFunc2
printf("2\n");
//uncomment to cause error: `test.c:13: error: expected expression before ‘)’ token`
//testFunc1();
}

#undef testFunc1
#undef testFunc2

int main() { testFunc1(); }

除了丑陋之外:它无法与第三方模块一起使用,你需要传递一个回调函数... - Matthieu M.

1

你无法在编译时完成这个任务,你需要跟踪整个应用程序的完整控制流程才能实现它(如果函数调用另一个函数,该函数又调用另一个函数,再调用原始函数...)。

改为在思考时间执行。在函数文档中添加大量注释。此外,也可以使用其他响应中提供的运行时解决方案之一-用于调试版本。

再补充一个:使用静态互斥锁来保护函数体(boost的scoped_lock会大大简化它)。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接