9

假设我们有以下代码:

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

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

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

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

编译器保证哪些函数调用不会缓存cross的值do_shutdown?很明显,如果函数是在外部声明的并且编译器无法访问它的定义,它就不能对其行为做出任何假设,因此它无法证明它没有访问do_shutdown. 如果编译器可以内联函数并证明它不能访问,那么即使在多线程设置中do_shutdown它也可以缓存吗?do_shutdown同一个编译单元中的非内联函数怎么样?

4

4 回答 4

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 或其他方式使其位置特殊,需要特定于实现的魔法。如果没有任何这样的魔法,原则上的实现有时可以知道这一点。

于 2010-12-18T03:47:22.550 回答
2

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

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

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

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

因此,在这种情况下,您必须使用机制来确保在调用过程中重新加载该值。请注意,由于硬件的复杂性,volatile关键字可能不足以确保正确的内存访问顺序 - 您应该依靠 pthread 或操作系统提供的 API 来确保这一点。(作为旁注,Microsoft 编译器的最新版本确实记录了volatile强制执行完整的内存屏障,但我读过的意见表明这不是标准所要求的)。

于 2010-12-18T03:47:28.917 回答
2

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

没有办法

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

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

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

于 2011-10-02T03:48:29.180 回答
0

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

于 2011-01-11T05:57:44.170 回答