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

假设我们有以下代码:

#include  #include  #include  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"); } 

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

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

实际上,在调用pthread_cond_wait() ,是否有任何C / C ++编译器在寄存器中缓存do_shutdown的值?

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

当然,目前的C和C ++标准对此主题没有任何说明。

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

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

线程无法实现,因为库涵盖了pthread实际可用的一些特定问题,但在撰写本文时(2004年)未在Posix标准中明确说明。 无论您的编译器编写者,还是为您的实现定义内存模型的人,都同意Boehm“可用”的含义,从而允许程序员“令人信服地说明程序的正确性”,这一点变得非常重要。

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

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

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

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

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

由于do_shutdown具有外部链接,因此编译器无法知道调用中发生了什么(除非它对所调用的函数具有完全可见性)。 因此,在调用之后,它必须重新加载值(易变或非线程与此无关)。

据我所知,除了标准用于定义表达式行为的(单线程)抽象机器表明在表达式中访问变量时需要读取变量,标准中没有直接说明这一点。 该标准允许仅在行为被certificate“好像”重新加载时才能优化读取变量。 只有当编译器知道函数调用没有修改该值时,才会发生这种情况。

也不是说pthread库确实对各种函数的内存障碍做出了某些保证,包括pthread_cond_wait() : 使用pthread互斥锁保护变量是否保证它也没有被缓存?

现在,如果do_shutdown是静态的(没有外部链接)并且你有几个线程使用在同一模块中定义的静态变量(即,静态变量的地址从未被传递给另一个模块),那可能是一个不同的故事。 例如,假设您有一个使用此类变量的函数,并启动了为该函数运行的多个线程实例。 在这种情况下,符合标准的编译器实现可能会在函数调用之间缓存该值,因为它可以假设没有其他任何东西可以修改该值(标准的抽象机器模型不包括线程)。

因此,在这种情况下,您必须使用机制来确保在整个调用中重新加载该值。 请注意,由于硬件错综复杂, volatile关键字可能不足以确保正确的内存访问顺序 – 您应该依赖pthreads或OS提供的API来确保这一点。 (作为旁注,微软编译器的最新版本确实记录了volatile强制执行完全内存障碍,但我已经阅读了表明标准不要求的意见)。

挥手的答案都是错的。 抱歉要严厉。

没有办法

编译器可以假设知道pthread_cond_wait()不会修改do_shutdown。

如果您有不同的do_shutdown ,请出示certificate:一个完整​​的C ++程序,使得不是为MT设计的编译器可以推断出pthread_cond_wait不会修改do_shutdown

这是荒谬的,编译器无法理解pthread_函数的作用,除非它具有POSIX线程的内置知识。

从我自己的工作中,我可以说是的,编译器可以跨pthread_mutex_lock / pthread_mutex_unlock缓存值。 我花了一个周末的大部分时间来查找一些代码中的错误,这些错误是由一组指针缓存引起的,并且对于需要它们的线程不可用。 作为一个快速测试,我将分配包装在互斥锁定/解锁中,并且线程仍然无法访问正确的指针值。 将指针赋值和关联的互斥锁定移动到单独的函数确实解决了问题。