线程之间是否存在共享变量的编译器优化问题?

Cor*_*lks 10 c c++ multithreading pthreads

请考虑以下示例.目标是使用两个线程,一个用于"计算"一个值,另一个用于消耗和使用计算值(我试图简化这个).计算线程通过使用条件变量向另一个线程发信号通知该值已经计算并准备就绪,之后等待线程消耗该值.

// 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;
}
Run Code Online (Sandbox Code Playgroud)

我的主要问题与编译器进行的潜在优化有关.是否允许编译器进行棘手的优化并"优化"程序流,以便发生以下情况:

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;
}
Run Code Online (Sandbox Code Playgroud)

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


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

caf*_*caf 9

不,编译器不会允许缓存的价值data->spaceHit跨越调用pthread_cond_wait()pthread_mutex_unlock().这些都被特别称为"与其他线程同步内存的函数",它必须充当编译器障碍.

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


Kaz*_*Kaz 6

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

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

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

条件的信号是无状态的,因此等待线程可以永久阻塞.仅仅因为你pthread_cond_signalgetValue输入线程中无条件地调用并不意味着watchValue会在等待中失败.它可能getValue首先执行,而spaceHit不是设置.然后watchValue进入互斥锁,看到它spaceHit是假的并执行可能无限期的等待.(唯一可以拯救它的是诡异的唤醒,具有讽刺意味的是,因为没有循环.)

基本上你似乎寻找的逻辑是一个简单的信号量:

// Consumer:
wait(data_ready_semaphore);
use(data);

// Producer:
data = produce();
signal(data_ready_semaphore);
Run Code Online (Sandbox Code Playgroud)

在这种交互方式中,我们不需要互斥体,这是通过你的无保护使用暗示data->spaceHitwatchValue.更具体地说,使用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);
Run Code Online (Sandbox Code Playgroud)

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

PS也pthread_cond_signal不必在互斥体内部.它可能会调用操作系统,因此只需要保护共享变量的长互斥保护区域只需要少量机器指令就可以吹掉数百个机器周期,因为它包含信令调用.