C 为什么这种竞态条件只发生在-O3中,并且只发生在某个看似等价的代码顺序中?
我已经实现了一个不可变的哈希映射和附带的STM容器,其灵感来自clojure的C 为什么这种竞态条件只发生在-O3中,并且只发生在某个看似等价的代码顺序中?,c,optimization,race-condition,reference-counting,C,Optimization,Race Condition,Reference Counting,我已经实现了一个不可变的哈希映射和附带的STM容器,其灵感来自clojure的atom,也就是说,类似于C++的std::unique_ptr,它通过一个指针管理(但不一定拥有)另一个对象,该指针可以传递和交换,并且只减少托管对象的引用计数,而不是彻底销毁它。当运行一些用-O3编译的测试代码时,我开始注意到错误的结果 比较和交换的代码如下所示: hashmap*update(stm*stm,update\u回调cb,void*user\u参数) { 而(1){ 锁(stm->mutex); ha
atom
,也就是说,类似于C++的std::unique_ptr
,它通过一个指针管理(但不一定拥有)另一个对象,该指针可以传递和交换,并且只减少托管对象的引用计数,而不是彻底销毁它。当运行一些用-O3
编译的测试代码时,我开始注意到错误的结果
比较和交换的代码如下所示:
hashmap*update(stm*stm,update\u回调cb,void*user\u参数)
{
而(1){
锁(stm->mutex);
hashmap*current=stm->reference;
增量_ref_count(当前);//第6行
解锁(stm->mutex);
hashmap*aspirant=cb(当前,用户参数);//返回一个新的线程本地实例
增量参考计数(有志者);
//位置1
锁(stm->mutex);
如果(current==stm->reference){//success,在我们忙的时候没有修改
stm->reference=aspirant;
increment_ref_count(aspirant);//stm现在有一个引用
解锁(stm->mutex);
//立场2.1
递减_ref_count(当前);//在第6行获取的释放引用
递减_ref_count(当前);//stm不再有引用
回归上进者;
}否则{//引用已修改,请循环并重试
解锁(stm->mutex);
//立场2.2
递减_ref_count(当前);//在第6行获取的释放引用
减量\u ref\u计数(aspirant);//ref\u计数现在为零,aspirant自由了
}
}
}
递增
和递减
中的参考计数-/原子地减少hashmap的参考计数。如果计数由于递减而降为零,hasmap不久后将被释放
为引用计数指针设计STM容器的挑战主要是获取引用和增加计数器原子,这就是我在这里使用锁的原因
作为测试,我使用hashmap+STM来计算单词列表中的出现次数
如果我按照这里发布的代码运行代码,则不会出现竞争条件现在我的问题来了:如果我移动减量\u ref\u count(当前);//对于if/else
中的第6行,远离位置2.1/2.2
(位于第二个锁定区域的正后方),并将其放置在位置1
(位于第二个锁定区域的正前方),突然字数开始不正确,我不知道为什么
我的论点是:a)我在第二个关键区域不使用current
,b)因此,在比较和交换之前或之后释放引用并不重要
显然,我有一个解决问题的方法;只需将减量保持在现在的位置,但我真的很想了解为什么会发生这种情况
在“Linux Windows子系统”上编译为:gcc-Wall-Wcast align-Wswitch enum-Wswitch default-Winit self-pedantic-O3-DNDEBUG-std=gnu11
。我发现了问题,甚至吸取了“教训”。首先,让我重新发布代码的损坏版本,以便更容易地讨论:
1 | hashmap *update(stm *stm, update_callback cb, void *user_args)
2 | {
3 | while (1) {
4 | lock(stm->mutex);
5 | hashmap *current = stm->reference;
6 | increment_ref_count(current); // Line 6
7 | unlock(stm->mutex);
8 |
9 | hashmap *aspirant = cb(current, user_args); // returns a new, thread local instance
10 | increment_ref_count(aspirant);
11 |
12 | decrement_ref_count(current); // current might get free'd here
13 |
14 | lock(stm->mutex);
15 | if (current == stm->reference) { // success, no modification while we were busy
16 | stm->reference = aspirant;
17 | increment_ref_count(aspirant); // stm now has a reference
18 | unlock(stm->mutex);
19 |
20 |
21 |
22 |
23 | decrement_ref_count(current); // stm no longer has a reference
24 | return aspirant;
25 |
26 | } else { // reference was modified, loop and try again
27 | unlock(stm->mutex);
28 |
29 |
30 |
31 |
32 | decrement_ref_count(aspirant); // ref_count now at zero, aspirant is free'd
33 | }
34 | }
35 | }
问题基本上是,在第15行我使用了一个地址,我已经放弃了对它的控制。我假设,如果另一个线程要修改stm->reference
,它将使用不同的地址进行修改。相反,发生了以下情况:当我的线程在第14行等待锁时,current
散列映射被解除分配,一个新的散列映射被分配到相同的内存地址,并成功地存储到stm->reference
。然后我的线程将继续,发现current==stm->reference
,错误地得出结论,自获取引用并执行交换后,stm->reference
没有发生任何更改,因此丢失了一些更新
这里的教训是:永远不要使用免费的指针,即使只是用来比较地址
这似乎缺少更多的上下文。什么是current
?@tadmancurrent
是stm
当前指向的哈希映射实例。如果此函数中当前没有线程(update
),并且没有线程获取用于读取的引用,则其引用计数为1,因为stm
是指向该哈希映射实例的唯一对象。我不精通C++,但是我所说的“STM”与线程安全<代码> STD::UnQuyJPPT类似。当你在代码中某个地方有不明确的行为时,随机代码突然中断或在你四处乱说各种语句时会发生。通常与您移动的代码无关。或者是堆栈溢出,这在这里看起来不太可能。但是你在这里发布的只是一堆函数调用——要找到bug,你需要深入挖掘那些函数,或者,你正在使用。这是哪一个编译器?@Lundin-Oh-boy。。。你对我从哪里或如何开始寻找UB有什么建议吗?我使用大量的-W
标志(请参见更新的问题)和gcc进行编译,结果是干净的。