C/C++编译器是否可以通过pthread库调用合法地在寄存器中缓存变量?

Tim*_*ell 9 c c++ concurrency standards sequence-points

假设我们有以下代码:

#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>

void guarantee(bool cond, const char *msg) {
    if (!cond) {
        fprintf(stderr, "%s", msg);
        exit(1);
    }
}

bool do_shutdown = false;   // Not volatile!
pthread_cond_t shutdown_cond = PTHREAD_COND_INITIALIZER;
pthread_mutex_t shutdown_cond_mutex = PTHREAD_MUTEX_INITIALIZER;

/* Called in Thread 1. Intended behavior is to block until
trigger_shutdown() is called. */
void wait_for_shutdown_signal() {

    int res;

    res = pthread_mutex_lock(&shutdown_cond_mutex);
    guarantee(res == 0, "Could not lock shutdown cond mutex");

    while (!do_shutdown) {   // while loop guards against spurious wakeups
        res = pthread_cond_wait(&shutdown_cond, &shutdown_cond_mutex);
        guarantee(res == 0, "Could not wait for shutdown cond");
    }

    res = pthread_mutex_unlock(&shutdown_cond_mutex);
    guarantee(res == 0, "Could not unlock shutdown cond mutex");
}

/* Called in Thread 2. */
void trigger_shutdown() {

    int res;

    res = pthread_mutex_lock(&shutdown_cond_mutex);
    guarantee(res == 0, "Could not lock shutdown cond mutex");

    do_shutdown = true;

    res = pthread_cond_signal(&shutdown_cond);
    guarantee(res == 0, "Could not signal shutdown cond");

    res = pthread_mutex_unlock(&shutdown_cond_mutex);
    guarantee(res == 0, "Could not unlock shutdown cond mutex");
}
Run Code Online (Sandbox Code Playgroud)

符合标准的C/C++编译器是否可以do_shutdown通过调用来缓存寄存器中的值pthread_cond_wait()?如果没有,哪些标准/条款保证这一点?

编译器可以假设知道pthread_cond_wait()不会修改do_shutdown.这似乎不太可能,但我知道没有任何标准可以阻止它.

实际上,任何C/C++编译器都会do_shutdown在调用时缓存寄存器中的值pthread_cond_wait()吗?

哪个函数调用是编译器保证不缓存do_shutdown跨越的值?很明显,如果函数是在外部声明的,并且编译器无法访问其定义,则它必须不对其行为进行假设,因此无法证明它不能访问do_shutdown.如果编译器可以内联函数并证明它不能访问do_shutdown,那么它是否可以do_shutdown在多线程设置中缓存?那个同一编译单元中的非内联函数怎么样?

Ste*_*sop 6

当然,目前的C和C++标准在这个问题上没有任何说明.

据我所知,Posix仍然避免正式定义并发模型(我可能已经过时了,但在这种情况下,我的答案仅适用于早期的Posix版本).因此它所说的内容必须有点同情 - 它没有准确地列出这方面的要求,但实施者应该"知道它意味着什么"并做一些使线程可用的东西.

当标准表示互斥锁"同步内存访问"时,实现必须假设这意味着在一个线程中锁定下进行的更改将在其他线程的锁定下可见.换句话说,同步操作包括这种或那种类型的内存屏障是必要的(尽管不够),并且内存屏障的必要行为是它必须假设全局变量可以改变.

线程无法实现,因为库涵盖了pthread实际可用所需的一些特定问题,但在撰写本文时(2004年)未在Posix标准中明确说明.无论你的编译器 - 编写者,还是为你的实现定义内存模型的人,都同意Boehm"可用"的意思,让程序员"有说服力地说明程序的正确性",这一点变得非常重要.

请注意,Posix不保证一致的内存缓存,因此如果您的实现反过来希望缓存do_something在代码中的寄存器中,那么即使您将其标记为volatile,也可能反过来选择不在同步操作之间弄脏CPU的本地缓存和阅读do_something.因此,如果编写器线程在具有自己的缓存的不同CPU上运行,那么即使这样,您也可能看不到更改.

这是(一个原因)为什么线程不能仅仅作为库来实现.这种仅从本地CPU缓存中获取volatile全局的优化在单线程C实现[*]中是有效的,但会破坏多线程代码.因此,编译器需要"了解"线程,以及它们如何影响其他语言功能(例如,在pthreads之外的例子:在Windows上,缓存始终是连贯的,Microsoft阐明了它volatile在多线程代码中授予的附加语义) .基本上,你必须假设如果你的实现遇到了提供pthreads函数的麻烦,那么就会遇到定义一个可操作的内存模型的麻烦,其中锁实际上同步了内存访问.

如果编译器可以内联函数并证明它不能访问do_shutdown,那么它是否可以在多线程设置中缓存do_shutdown?那个同一编译单元中的非内联函数怎么样?

对所有这些都是 - 如果对象是非易失性的,并且编译器可以证明此线程不修改它(通过其名称或通过别名指针),并且如果没有发生内存障碍,那么它可以重用以前的值.当然,有时也会有其他特定于实现的条件有时会阻止它.

[*]只要实现知道全局不位于某个"特殊"硬件地址,该地址要求读取始终通过缓存到主存储器,以便查看任何硬件操作影响该地址的结果.但是要在任何这样的位置设置全局,或者使用DMA或其他任何东西使其位置特殊,需要特定于实现的魔法.没有任何这样的魔法,原则上的实施有时可以知道这一点.