调试另一个线程的实例改变我的数据

时间:2010-03-17 11:53:35

标签: c++ c visual-studio-2008 multithreading debugging

我有一个庞大的全球结构阵列。阵列的某些区域与各个线程相关联,并且这些线程可以修改其阵列的区域,而无需使用关键部分。但是阵列中有一个特殊区域,所有线程都可以访问它。访问数组这些部分的代码需要仔细使用关键部分(每个数组元素都有自己的关键部分),以防止两个线程同时写入结构的任何可能性。

现在我有一个我试图追逐的神秘错误,它正在发生不可预测且很少发生。似乎其中一个结构充满了一些不正确的数字。一个明显的解释是,当应该排除这个数字时,意外地允许另一个线程设置这个数字。

不幸的是,似乎几乎无法跟踪此错误。每次出现坏数据的数组元素都不同。我希望能够做的是为bug设置一些陷阱,如下所示:我将为数组元素N输入一个关键部分,然后我知道没有其他线程应该能够触及数据,然后(直到我退出关键部分)为调试工具设置某种标志,说“如果有任何其他线程试图在这里更改数据,请打破并向我显示令人讨厌的源代码补丁”......但我怀疑没有这样的工具存在。 ..或者是吗?或者我应该采用一些完全不同的调试方法。

4 个答案:

答案 0 :(得分:2)

如何使用透明的互斥类包装数据?然后,您可以应用其他锁定状态检查。

class critical_section;

template < class T >
class element_wrapper
{
public:
    element_wrapper(const T& v) : val(v) {}
    element_wrapper() {}
    const element_wrapper& operator = (const T& v) {
#ifdef _DEBUG_CONCURRENCY 
        if(!cs->is_locked())
            _CrtDebugBreak();
#endif
        val = v;
        return *this;
    }
    operator T() { return val; }
    critical_section* cs;
private:
    T val;
};

至于关键部分的实施:

class critical_section
{
public:
    critical_section() : locked(FALSE) {
        ::InitializeCriticalSection(&cs);
    }
    ~critical_section() {
        _ASSERT(!locked);
        ::DeleteCriticalSection(&cs);
    }
    void lock() {
        ::EnterCriticalSection(&cs);
        locked = TRUE;
    }
    void unlock() {
        locked = FALSE;
        ::LeaveCriticalSection(&cs);
    }
    BOOL is_locked() {
        return locked;
    }
private:
    CRITICAL_SECTION cs;
    BOOL locked;
};

实际上,可以使用critical_section::locked(如果成功后跟::TryEnterCriticalSection)而不是自定义::LeaveCriticalSection标志来确定是否拥有关键部分。虽然,上面的实现几乎一样好。

所以适当的用法是:

typedef std::vector< element_wrapper<int> > cont_t;

void change(cont_t::reference x) { x.lock(); x = 1; x.unlock(); }

int main()
{
    cont_t container(10, 0); 

    std::for_each(container.begin(), container.end(), &change);
}

答案 1 :(得分:0)

我知道处理此类错误的两种方法:

1)反复阅读代码,寻找可能的错误。我可以考虑两个可能导致这种情况的错误:不同步的访问或不正确的内存地址写入。也许你有更多的想法。

2)记录,记录日志记录。在每个关键位置添加许多可选跟踪(OutputDebugString或日志文件),其中包含足够的信息 - 索引,变量值等。最好使用#ifdef添加此跟踪。重现错误并尝试从日志中了解会发生什么。

答案 2 :(得分:0)

您最好(最快)的赌注仍然是修改互斥代码。正如你所说,这是一个明显的解释 - 为什么不试图真正找到解释(通过逻辑)而不是额外的提示(通过编码)可能会产生不确定的?如果代码审查没有结果有用,您仍然可以使用互斥代码并将其用于测试运行。第一次尝试不应该重现系统中的错误,而是确保正确实现互斥锁 - 实现线程(从2开始向上),所有线程都试图一次又一次地访问相同的数据结构,并且每个都有一个随机的小延迟他们让他们在时间线上抖动。如果这个测试导致了一个你在代码中无法识别的有缺陷的互斥锁,那么你已经成为一些架构依赖效应的受害者(可能是工程重新排序,多核缓存不兼容等),并且需要找到另一个互斥实现。如果OTOH你在互斥锁中发现了一个明显的错误,试着在你的真实系统中利用它(检测你的代码,以便更频繁地出现错误),这样你就可以确保它确实是原始问题的原因。 / p>

答案 3 :(得分:0)

我在蹬踏工作时正在思考这个问题。处理此问题的一种可能方法是,当未通过关键部分所有权主动访问和保护存储器的部分时,使其成为只读存储器。这假设问题是由于线程在没有相应的临界区时写入内存引起的。

这有一些限制,阻止它工作。最重要的是,我认为你只能逐页设置权限(我相信4K)。因此,您可能需要对分配方案进行一些非常具体的更改,以便缩小相应的部分以进行保护。第二个问题是,如果另一个线程主动拥有临界区,它将不会捕获写入内存的流氓线程。但如果关键部分不属于它,它会捕获它并导致立即访问冲突。

我们的想法是将您的EnterCriticalSection调用更改为:

EnterCriticalSection()
VirtualProtect( … PAGE_READWRITE … );

将LeaveCriticalSection调用更改为:

VirtualProtect( … PAGE_READONLY … );
LeaveCriticalSection()

以下代码块显示对VirtualProtect

的调用
int main( int argc, char* argv[] 1
{
  unsigned char *mem;
  int i;
  DWORD dwOld;

  // this assume 4K page size
  mem = malloc( 4096 * 10 );
  for ( i = 0; i < 10; i++ )
     mem[i * 4096] = i;
  // set the second page to be readonly.  The allocation from malloc is
  // not necessarily on a page boundary, but this will definitely be in
  // the second page.
  printf( "VirtualProtect res = %d\n",
          VirtualProtect( mem + 4096,
                          1, // ends up setting entire page
                          PAGE_READONLY, &dwOld ));
  // can still read it
  for ( i = 1; i < 10; i++ )
     printf( "%d ", mem[i*4096] );
  printf( "\n" );

  // Can write to all but the second page
  for ( i = 0; i < 10; i++ )
     if ( i != 1 ) // avoid second page which we made readonly
        mem[i] = 1;
  // this causes an access violation
  mem[4096] = 1;
}