【问题标题】:gcc Atomic Operations Causing SEGVgcc 原子操作导致 SEGV
【发布时间】:2011-09-28 16:52:27
【问题描述】:

我在多线程应用程序中使用 gcc 原子操作已经有一段时间了,昨天遇到了一个我无法解释的有趣场景。这些原子函数是重载的,可以使用 1、2、4 或 8 字节宽的数据类型。特别是,我成功地使用了 bool_compare_and_swap (CAS) 操作。这里出现的问题是可重复的,并且仅在我编译优化代码 (O3) 时发生。当我编译未优化(O0)时,不会出现此问题。请记住这一点,因为我相信优化器在我在这里展示的情况下正在做一些时髦的事情。

我通常做的是创建一个包含命名类型(chars、shorts 等)和数据类型的联合,该数据类型是“适合”该结构到一个对象中的适当大小(即 long long),在这种情况下,我在一个联合中有一个 8 字节(long long),其对应的结构包含位域。示例数据类型定义如下所示。这个想法是位域可以通过赋值语句来改变,一旦所有的赋值完成,8字节的数据类型就是那个被CAS'd的数据类型。具体来说,我正在使用:

bool __sync_bool_compare_and_swap (type *ptr, type oldval type newval, ...)

这样封装在宏中:

define THD_CAS(ptr, oldVal, newVal) __sync_bool_compare_and_swap(ptr, oldVal, newVal)

我有一个定义如下的结构:

typedef union _TSynchro {
    struct {
        int          *pFirstSynchWork;
        unsigned short   idTransaction;
        unsigned short       fNewTrans  :1,
            fFileBad        :1,                 
            fOpComplete :1,                 
            fCancelWork :1,                     
            fPurged     :1,
            fStatRequired   :1;
    } Data;
    // above struct is overlayed by this struct so we can CAS all values with a single 64 bit cas
    long long           n64;
} TSynchro;

所以,我在代码中有一个并发循环(while(1)),它获取当前数据值的“快照”并存储在旧数据中,在数据的新副本(新)中设置位,然后尝试CAS操作。如果 CAS 成功,我就是更改数据的线程,我会跳出循环。如果 CAS 失败,则其他线程更改了我下面的数据,然后我重试,抓取当前数据的另一个“快照”。

void NewSynchro(TSynchro *pSynchro)
{
    volatile TSynchro   New;
    volatile TSynchro Old;

    while (1) // concurrency loop
    {
        Old.n64 = pSynchro->n64;
        New.n64 = Old.n64;
        New.Data.fOpComplete = 1;
        New.Data.fStatRequired = 0;
        if (fFileBad)
        {
            New.Data.fFileBad = 1;
        }
        else
        {
            New.Data.fReleased = 0;
            New.Data.fFileBad = 0;
        }

        if (THD_CAS(&pSynchro->n64, Old.n64, New.n64))
            break;  // success
    }
}

现在,有趣的是...看到我将旧的和新的声明为 volatile 了吗?好吧,如果旧版和新版都没有 volatile 修改,那么当我在调用 NewSynchro() 之后进入下一个函数调用时,我会得到一个 SEGV。如果 OLD 或 NEW 或 BOTH 都有 volatile 修饰符,则应用程序代码永远不会 SEGV。在开发中,我现在只运行 1 个线程(没有真正的更改值的争用威胁),所以我还尝试摆脱 CAS 并用一个简单的赋值替换(即 pSynchro->n64 = New.n64 ),并且应用程序也运行良好。

我一直在其他地方使用 8 字节 CAS,它似乎工作正常。这里的一个区别是我认为这是我第一次在结构中使用位域。

想法?

【问题讨论】:

    标签: gcc atomic volatile


    【解决方案1】:

    让我提出一些想法: 联合通常用于非此即彼:long long 或 struct。在这种情况下,您同时使用两者,它就像在大多数简单处理器上一样工作。但是,如果您有一个复杂的流水线处理器,您可能会遇到内存屏障或类似问题的必要性。 更具体地说:在位域中设置一个位是读-修改-写操作。当您按这样的顺序执行这些操作时,可能会出现问题:

    New.n64 = Old.n64;
    New.Data.fOpComplete = 1;
    

    由于联合,设置位的读取可以在 n64 的写入完成之前开始。这种流水线可以通过编译器插入流水线刷新或内存屏障来抵消。但是使用联合,编译器可以假设元素是分开的:“联合一次只能包含一个它的组件值。” (H&S5, 5.7.1) 它不会倾向于插入任何刷新/屏障,尤其是在使用像 -O3 这样的激进优化时。 并且为New 使用 volatile 还将指导编译器(即使使用 -O3)以确保正确分离读/写。 为什么声明Old volatile 会阻止你的问题,我不能说。然后我必须查看并比较生成的汇编代码。

    【讨论】:

    • 你是说编译器会被别名所迷惑​​,编译器会对上面的代码重新排序?我不明白 cpu 如何以产生不同结果的方式重新排序编译器生成的 mem 读/写操作。
    • @johnnycrash 不,不是编译器;编译器不会重新排序属于同一个对象的这些指令。但是处理器可能会这样做,如果它具有流水线架构,并且只有在第二条指令的读入已经完成后才会执行第一条指令的回写。
    • 我想这是我的观点/问题。由于 .n64 或 .Data.fOpComplete,cpu 不知道编译器生成的代码来读取/写入内存。 cpu 看到内存位置。您是说使用联合两种方式会导致 编译器 做出别名假设,从而导致代码排序不正确。您是说 cpu 可以将两次写入重新排序到同一内存位置,以便覆盖第二次写入?对我来说,这似乎是一个非常糟糕的主意!
    • 哦,我明白了。不,当使用优化并且没有使用volatile 时,编译器(对于流水线处理器)不会倾向于插入流水线停顿或刷新或内存屏障,而只是让处理器按照它想要的速度运行。我将编辑我的答案以使其更清楚。无论如何,编译器都不会对同一个对象的读/写操作重新排序。即使涉及工会,也不允许这样做。
    猜你喜欢
    • 2011-12-13
    • 2013-08-13
    • 2022-01-12
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-04-12
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多