【问题标题】:Are there compiler optimization issues with sharing variables between threads?线程之间共享变量是否存在编译器优化问题?
【发布时间】:2013-05-24 03:29:50
【问题描述】:

考虑以下示例。目标是使用两个线程,一个用于“计算”一个值,另一个使用计算值(我试图简化这一点)。计算线程使用条件变量向另一个线程发出信号,表明该值已计算完毕并准备就绪,之后等待线程使用该值。

// Hopefully this is free from errors, if not, please point them out so I can fix
// them and we can focus on the main question
#include <pthread.h>
#include <stdio.h>

// The data passed to each thread. These could just be global variables.
typedef struct ThreadData
{
  pthread_mutex_t mutex;
  pthread_cond_t cond;
  int spaceHit;
} ThreadData;

// The "computing" thread... just asks you to press space and checks if you did or not
void* getValue(void* td)
{
  ThreadData* data = td;

  pthread_mutex_lock(&data->mutex);

  printf("Please hit space and press enter\n");
  data->spaceHit = getchar() == ' ';
  pthread_cond_signal(&data->cond);

  pthread_mutex_unlock(&data->mutex);

  return NULL;
}

// The "consuming" thread... waits for the value to be set and then uses it
void* watchValue(void* td)
{
  ThreadData* data = td;

  pthread_mutex_lock(&data->mutex);
  if (!data->spaceHit)
      pthread_cond_wait(&data->cond, &data->mutex);
  pthread_mutex_unlock(&data->mutex);

  if (data->spaceHit)
      printf("You hit space!\n");
  else
    printf("You did NOT hit space!\n");

  return NULL;
}

int main()
{
  // Boring main function. Just initializes things and starts the two threads.
  pthread_t threads[2];
  pthread_attr_t attr;
  ThreadData data;
  data.spaceHit = 0;

  pthread_mutex_init(&data.mutex, NULL);
  pthread_cond_init(&data.cond, NULL);

  pthread_attr_init(&attr);
  pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_JOINABLE);
  pthread_create(&threads[0], &attr, watchValue, &data);
  pthread_create(&threads[1], &attr, getValue, &data);

  pthread_join(threads[0], NULL);
  pthread_join(threads[1], NULL);

  pthread_attr_destroy(&attr);
  pthread_mutex_destroy(&data.mutex);
  pthread_cond_destroy(&data.cond);

  return 0;
}

我的主要问题与编译器所做的潜在优化有关。是否允许编译器进行棘手的优化并“优化”程序流,从而发生以下情况:

void* watchValue(void* td)
{
  ThreadData* data = td;

  pthread_mutex_lock(&data->mutex);
  if (!data->spaceHit) // Here, it might remember the result of data->spaceHit
      pthread_cond_wait(&data->cond, &data->mutex);
  pthread_mutex_unlock(&data->mutex);

  if (remember the old result of data->spaceHit without re-getting it)
      printf("You hit space!\n");
  else
    printf("You did NOT hit space!\n");
  // The above if statement now may not execute correctly because it didn't
  // re-get the value of data->spaceHit, but "remembered" the old result
  // from the if statement a few lines above

  return NULL;
}

我有点偏执,编译器的静态分析可能会确定data-&gt;spaceHit 在两个if 语句之间没有变化,因此证明使用data-&gt;spaceHit 的旧值而不是重新获取新值是合理的.我对线程和编译器优化知之甚少,无法知道这段代码是否安全。是吗?


注意:我是用 C 语言编写的,并将其标记为 C 和 C++。我在 C++ 库中使用它,但由于我使用 C 线程 API(pthreads 和 Win32 线程)并且可以选择将 C 嵌入到 C++ 库的这一部分中,因此我将其标记为 C 和 C++ .

【问题讨论】:

    标签: c++ c multithreading pthreads


    【解决方案1】:

    (稍后编辑:看起来后续的答案已经为这个查询提供了更好的答案。我将把这个答案留在这里作为一种方法的参考也回答这个问题。建议您是否推荐其他方法。)

    类型 ThreadData 本身不是易失性的。

    main() 中将其实例化为“数据”是易变的。 getValue() 和 watchValueValue() 中的指针 'data' 也指向 'ThreadData' 类型的 volatile 版本。

    虽然我喜欢第一个答案,因为它的紧密性,重写

    ThreadData data;  // main()
    ThreadData* data; // getValue(), watchValueValue()
    

    volatile ThreadData data;  // main()
    volatile ThreadData* data; // getValue(), watchValueValue()
                               // Pointer `data` is not volatile, what it points to is volatile.
    

    可能会更好。它将确保对 ThreadData 成员的任何访问总是被重新读取而不是优化。如果您向ThreadData 添加其他字段,同样会受到保护。

    【讨论】:

    • 要么我没有正确理解你,要么你搞错了。 datamain() 中的实例化不是volatile。线程函数中的tddata 指针也没有标记volatile。你能澄清一下吗?
    • 我建议将数据(主要标记为 voltile。这将阻止编译器对“数据”进行任何优化,因为它可以随时更改。
    • 只有ThreadData::spaceHit 需要标记为易失性(直接在struct 定义中)。当然标记data volatile 也会使其所有成员都变得易变,但最好只标记ThreadData::spaceHit volatile,因为这样您就不会冒险忘记它(否则您必须将每个ThreadData 实例标记为易失性并确保所有您的指针保留了限定符,这很容易出错)。
    • @chux:也很好。但是,我认为常量性通常是实例或别名的问题(有些需要,有些不需要),而在这种情况下,由于该特定数据成员的多线程使用,需要易失性。
    • 至少如果编译C并发访问线程环境中的一个(共享)变量需要用互斥体(锁)保护,不需要声明volatile,互斥体和编译器应尽一切努力保证变量的值在读取时保持一致,在写入时保持一致。
    【解决方案2】:

    一般来说,线程之间共享数据不仅存在编译器优化问题,而且当这些线程位于可以乱序执行指令的不同处理器上时,还会存在硬件优化问题。

    但是,pthread_mutex_lockpthread_mutex_unlock 函数不仅必须破坏编译器缓存优化,还必须破坏任何硬件重新排序优化。如果线程 A 准备了一些共享数据,然后通过执行解锁“发布”它,这必须与其他线程保持一致。例如,不能在另一个处理器上出现锁被释放,但共享变量的更新尚未完成。所以函数必须执行任何必要的内存屏障。如果编译器可以围绕函数调用移动数据访问,或者在寄存器级别缓存内容从而破坏一致性,那么所有这些都将是徒劳的。

    所以从这个角度来看,您拥有的代码是安全的。但是,它还有其他问题。 pthread_cond_wait 函数应始终在重新测试变量的循环中调用,因为任何原因都可能出现虚假唤醒。

    条件的信号是无状态的,所以等待线程可以永远阻塞。仅仅因为您在getValue 输入线程中无条件地调用pthread_cond_signal 并不意味着watchValue 将通过等待。 getValue 可能先执行,而 spaceHit 没有设置。然后watchValue 进入互斥体,看到spaceHit 为假,并执行一个可能是无限期的等待。 (具有讽刺意味的是,唯一可以挽救它的是虚假唤醒,因为没有循环。)

    基本上,您似乎正在寻找的逻辑是一个简单的信号量:

    // Consumer:
    wait(data_ready_semaphore);
    use(data);
    
    // Producer:
    data = produce();
    signal(data_ready_semaphore);
    

    在这种交互方式中,我们不需要互斥锁,这可以通过在您的watchValue 中不受保护地使用data-&gt;spaceHit 来暗示。更具体地说,使用 POSIX 信号量语法:

    // "watchValue" consumer
    sem_wait(&ready_semaphore);
    if (data->spaceHit)
      printf("You hit space!\n");
    else
      printf("You did NOT hit space!\n");
    
    // "getValue" producer
    data->spaceHit = getchar() == ' ';
    sem_post(&ready_semaphore);
    

    也许您简化为示例的真实代码可以只使用信号量。

    附: pthread_cond_signal 也不必在互斥锁内。它可能会调用操作系统,因此一个受互斥体保护的区域只需要几条机器指令就可以保护共享变量,因为它包含信号调用,所以可能会炸毁数百个机器周期。

    【讨论】:

    • 呃,SMP 不是这样工作的。如果其中任何一个被线程更新,底层硬件将确保所有缓存相同内存的缓存都将失效。英特尔的 QPI 必须非常努力才能实现这一目标。如果没有它,它将是一个 NUMA 系统,在这种情况下,您首先将无法使用共享内存。易失性和内存栅栏(编译器和硬件)的全部意义在于确保编译器不会将变量存储在寄存器中,并且 CPU 会以正确的顺序执行指令。之后 SMP 会整理出其余部分。
    • @bazza 你是对的;缓存一致性通常得到处理,那么为什么将它混入答案中。为什么在共享数据被解决之前锁可能看起来被解锁主要是因为乱序执行。最初只是第一段暗示了这一点;我修好了。
    • pthread_cond_signal 保留在互斥体中通常是个好主意。它使您无需等待。
    • 为什么在锁之外访问data-&gt;spaceHit 变量是个问题?它此时已经接收到信号,所以它确定它不会被另一个线程修改(因为它已经被修改和设置)。还是我什么都没看到?
    • @Hasturkun 将pthread_cond_signal 放在互斥锁中不需要解决与等待相关的任何竞争条件问题,因为pthread_cond_wait 释放互斥锁并以唤醒不会丢失的方式等待.为了避免与等待竞争(导致丢失唤醒),信号线程必须在发出信号之前锁定互斥体,但解锁和信号之间的顺序无关紧要。
    【解决方案3】:

    不,编译器不允许在对pthread_cond_wait()pthread_mutex_unlock() 的调用中缓存data-&gt;spaceHit 的值。这些都被特别称为"functions [which] synchronize memory with respect to other threads",它必须充当编译器屏障。

    要使编译器成为符合标准的 pthreads 实现的一部分,它不能在您给出的情况下执行该优化。

    【讨论】:

    • 编译器不会在调用任何其他系统调用或未知库函数时缓存值。
    猜你喜欢
    • 2011-06-23
    • 1970-01-01
    • 1970-01-01
    • 2021-01-27
    • 1970-01-01
    • 1970-01-01
    • 2014-08-07
    相关资源
    最近更新 更多