违反 std::map 运算符中的读取位置

Violation reading location in std::map operator[]

提问人:Marcin 提问时间:10/24/2008 最后编辑:Marcin 更新时间:10/24/2008 访问量:5762

问:

我在运行一些传给我的旧代码时遇到了问题。它在 99% 的时间内都可以工作,但偶尔,我注意到它会抛出“违规读取位置”异常。在进程的整个生命周期中,我有可变数量的线程可能会执行此代码。低出现频率可能表明存在竞争条件,但我不知道为什么在这种情况下会导致异常。以下是有问题的代码:

MyClass::Dostuff()
{
    static map<char, int> mappedChars;
    if (mappedChars.empty())
    {
       for (char c = '0'; c <= '9'; ++c)
       {
          mappedChars[c] = c - '0';
       }
    }
    // More code here, but mappedChars in not changed.
}

在映射的 operator[] 实现中,在第一次调用 operator[] 时(使用 STL 的 VS2005 实现)时,就会抛出异常。


mapped_type& operator[](const key_type& _Keyval)
{
    iterator _Where = this->lower_bound(_Keyval); //exception thrown on the first line
    // More code here
}

我已经尝试冻结 operator[] 中的线程并尝试让它们同时运行它,但我无法使用该方法重现异常。

你能想出为什么会抛出任何原因吗,而且只是一些时候?

(是的,我知道 STL 不是线程安全的,我需要在这里进行更改。我最好奇的是为什么我会看到我上面描述的行为。

根据要求,以下是有关异常的更多详细信息:
app15-51-02-0944_2008-10-23.mdmp 中 0x00639a1c (app.exe) 处未处理的异常:0xC0000005:读取位置0x00000004的访问冲突。

感谢大家为多线程问题提供解决方案,但这不是这个问题要解决的问题。是的,我知道所呈现的代码没有得到正确的保护,并且在它试图完成的事情上是矫枉过正的。我已经实施了它的修复程序。我只是想更好地理解为什么一开始就抛出这个异常。

C++ 多线程 异常 STL

评论

0赞 Tony Lee 10/24/2008
了解违规行为的地址可能会有所帮助。“this”可能为 null,它与地图本身无关。

答:

2赞 Henk 10/24/2008 #1

如果多个线程正在调用该函数,这将意味着初始化代码DoStuff

if (mappedChars.empty())

可以进入竞争条件。这意味着线程 1 进入函数,发现地图为空并开始填充它。然后线程 2 进入函数并发现映射不为空(但未完全初始化),因此愉快地开始读取它。因为两个线程现在都处于争用状态,但其中一个线程正在修改映射结构(即插入节点),所以将导致未定义的行为(崩溃)。

如果在检查映射之前使用同步原语,并在保证映射已完全初始化后释放,则一切都会好起来的。empty()

我通过谷歌看了一下,静态初始化确实不是线程安全的。因此,声明立即成为一个问题。正如其他人所提到的,最好在保证只有 1 个线程在初始化的生命周期内处于活动状态时完成初始化。static mappedChars

评论

0赞 Marcin 10/24/2008
请注意,写入线程尚未开始填充它。它抛出运算符 [] 的第一行。据我所知,lower_bound不会改变地图的内容。
0赞 Henk 10/24/2008
其他线程在做什么,是否有其他人也访问此共享变量?我不记得编译器为静态变量插入的代码本身是否是线程安全的。
3赞 Tim Stewart 10/24/2008 #2

mappedChars 是静态的,因此它由执行 DoStuff() 的所有线程共享。仅此一项就可能是您的问题。

如果必须使用静态映射,则可能需要使用互斥锁或关键部分来保护它。

就我个人而言,我认为为此目的使用地图是矫枉过正的。我会编写一个辅助函数,该函数接受一个字符并从中减去“0”。函数不会有任何线程安全问题。

5赞 Tony Lee 10/24/2008 #3

给定地址为“4”,则“this”指针可能为 null 或迭代器错误。您应该能够在调试器中看到这一点。如果这是 null,则问题不在于该函数,而在于谁在调用该函数。如果迭代器不好,那么这就是你提到的竞争条件。大多数迭代器不能容忍列表的更新。

好吧,等等 - 这里没有 FM。静态在首次使用时初始化。执行此操作的代码不是多线程安全的。一个线程正在进行初始化,而第二个线程认为它已经完成,但仍在进行中。结果是使用未初始化的变量。您可以在下面的程序集中看到这一点:

static x y;
004113ED  mov         eax,dword ptr [$S1 (418164h)] 
004113F2  and         eax,1 
004113F5  jne         wmain+6Ch (41141Ch) 
004113F7  mov         eax,dword ptr [$S1 (418164h)] 
004113FC  or          eax,1 
004113FF  mov         dword ptr [$S1 (418164h)],eax 
00411404  mov         dword ptr [ebp-4],0 
0041140B  mov         ecx,offset y (418160h) 
00411410  call        x::x (4111A4h) 
00411415  mov         dword ptr [ebp-4],0FFFFFFFFh

$S 1 在初始化时设置为 1。如果设置了 (004113F5),它会跳过初始化代码 - 冻结 fnc 中的线程将无济于事,因为此检查是在进入函数时完成的。这不是 null,但其中一个成员是 null。

通过将映射移出方法并作为静态移动到类中来修复。然后它将在启动时初始化。否则,您必须在调用 do DoStuff() 周围放置一个 CR。您可以通过在地图本身的使用周围放置一个 CR 来防止剩余的 MT 问题(例如,DoStuff 使用 operator[])。

评论

0赞 Marcin 10/24/2008
你是说地图是这个指针?映射是静态的,在 for 循环之外永远不会更改。
1赞 Tony Lee 10/24/2008
是的,这就是它的样子。但我同意,如果地图的唯一访问是 mappedChars[c],那就没有意义了。迭代器可能是由竞争条件导致的 null - 返回一个迭代器,映射由另一个线程更新,使其无效。
0赞 Michael Burr 10/24/2008
@me.yahoo.com :一个问题是需要构建静态地图。线程 A 出现并开始构建它,但在完成之前,线程 B 出现,认为它已经构建并开始使用它。砰。另一种情况是不同的线程认为它们需要构造。砰。
0赞 Tony Lee 10/25/2008
同意,另一种情况是它被构建两次。
1赞 Eclipse 10/24/2008 #4

当你进入多线程时,通常会有太多的事情要做,以确定事情变得糟糕的确切位置,因为它总是在变化。在很多情况下,在多线程情况下使用静态映射可能会变坏。

有关保护静态变量的一些方法,请参阅此线程。最好的办法可能是在启动多个线程来初始化它之前调用该函数一次。或者,或者将静态映射移出,并创建一个单独的初始化方法。

0赞 Adam Rosenfield 10/24/2008 #5

你有没有打电话提出一个不在范围内的论点?如果是这样,那么您无意中修改了地图,这可能会导致其他线程发生错误。如果使用映射中尚不存在的参数进行调用,则会将该键插入到映射中,其值等于值类型的默认值(如果为 0)。operator[]0..9operator[]int