遵循多线程环境中的指针

时间:2011-01-13 23:54:32

标签: c multithreading pthreads c99 compiler-optimization

如果我有一些类似的代码:

typedef struct {
    bool some_flag;

    pthread_cond_t  c;
    pthread_mutex_t m;
} foo_t;

// I assume the mutex has already been locked, and will be unlocked
// some time after this function returns. For clarity. Definitely not
// out of laziness ;)
void check_flag(foo_t* f) {
    while(f->flag)
        pthread_cond_wait(&f->c, &f->m);
}

C标准中是否有任何内容阻止优化器将check_flag重写为:

void check_flag(foo_t* f) {
    bool cache = f->flag;
    while(cache)
        pthread_cond_wait(&f->c, &f->m);
}

换句话说,每次循环时生成的代码是否都遵循f指针,或者编译器是否可以自由地取消引用?

如果 可以自由地将其拉出来,有什么方法可以阻止这种情况吗?我需要在某处撒一个volatile关键字吗?它不能是check_flag的参数,因为我打算在这个结构中有其他变量,我不介意编译器这样优化。

我可能要诉诸:

void check_flag(foo_t* f) {
    volatile bool* cache = &f->some_flag;
    while(*cache)
        pthread_cond_wait(&f->c, &f->m);
}

4 个答案:

答案 0 :(得分:7)

在一般情况下,即使没有涉及多线程,你的循环也是如此:

void check_flag(foo_t* f) {
    while(f->flag)
        foo(&f->c, &f->m);
}

编译器无法缓存f->flag测试。那是因为编译器无法知道函数(如上面的foo())是否可以改变f指向的任何对象。

在特殊情况下(foo()对编译器可见,并且传递给check_flag()的所有指针都知道没有别名或foo()可以修改)编译器可能能够优化检查。

但是,pthread_cond_wait()必须以阻止优化的方式实施。

请参阅Does guarding a variable with a pthread mutex guarantee it's also not cached?

您可能也对Steve Jessop对Can a C/C++ compiler legally cache a variable in a register across a pthread library call?

的回答感兴趣

但是你想在你自己的工作中把Boehm论文提出的问题放在多远,取决于你。据我所知,如果你想采取pthreads没有/不能做出保证的立场,那么你实质上就是认为pthreads是无用的(或至少不提供安全保证,我认为通过减少有相同的结果)。虽然这可能是最严格意义上的(如论文中所述),但它也可能不是一个有用的答案。我不确定除了基于Unix的平台上的pthreads之外你还有什么选择。

答案 1 :(得分:3)

通常,您应该在等待条件对象之前尝试锁定pthread互斥锁,因为pthread_cond_wait调用释放互斥锁(并在返回之前重新获取它)。所以,你的check_flag函数应该被重写,以符合pthread条件下的语义。

void check_flag(foo_t* f) {
    pthread_mutex_lock(&f->m);
    while(f->flag)
        pthread_cond_wait(&f->c, &f->m);
    pthread_mutex_unlock(&f->m);
}

关于是否允许编译器优化flag字段的读数的问题,answer比我更详细地解释了它。

基本上,编译器知道pthread_cond_waitpthread_mutex_lockpthread_mutex_unlock的语义。他知道在这种情况下他无法优化内存读取(在这个例子中调用pthread_cond_wait)。这里没有记忆障碍的概念,只是某种功能的特殊知识,以及在他们面前遵循的一些规则。

还有另一件事可以保护您免受处理器执行的优化。您的平均处理器能够重新排序内存访问(读/写),前提是语义是守恒的,并且它总是这样做(因为它允许提高性能)。但是,当多个处理器可以访问相同的内存地址时,这会中断。内存屏障只是处理器的指令,告诉它可以移动在屏障之前发出的读/写并在屏障之后执行它们。它现在已经完成了。

答案 2 :(得分:3)

如上所述,编译器可以自由地按照您描述的方式缓存结果,甚至可以以更微妙的方式缓存 - 通过将其放入寄存器。您可以通过创建变量volatile来阻止此优化。但这不一定足够 - 你不应该这样编码!你应该按规定使用条件变量(锁定,等待,解锁)。

尝试在图书馆周围工作很糟糕,但情况会变得更糟。或许阅读Hans Boehm关于PLDI 2005(“线程不能实现为图书馆”)的一般主题的论文,或者他的许多follow-on articles(导致修订的C ++内存模型)把敬畏上帝放在你身上,引导你回到直线和狭窄的地方:)。

答案 3 :(得分:1)

易失性就是为了这个目的。依赖于编译器来了解pthread编码实践对我来说似乎有点疯狂;编译器这些天非常聪明。事实上,编译器可能会看到您正在循环测试变量,并且不会因为这个原因而将其缓存在寄存器中,而不是因为它看到您使用pthreads。如果你真的在乎,请使用volatile。

有点小搞笑。我们有一个VOLATILE #define,它是“易变的”(当我们认为bug不可能是我们的代码时......)或空白。当我们认为由于优化器杀死我们而导致崩溃时,我们#define它“volatile”,它将volatile放在几乎所有东西面前。然后我们测试看问题是否消失。到目前为止......错误一直是开发人员,而不是编译器!谁有想过!?我们开发了一个高性能的“非锁定”和“非阻塞”线程库。我们有一个测试平台,可以达到每秒数千场比赛的程度。所以,我们从未发现需要挥发的问题!到目前为止,gcc从未在寄存器中缓存共享变量。呀......我们也很惊讶。我们还在等待机会使用volatile!