C++ 在线程之间共享变量是否存在编译器优化问题?
考虑下面的例子。目标是使用两个线程,一个用来“计算”一个值,另一个用来消耗和使用计算出的值(我试图简化这个)。计算线程向另一个线程发出信号,表示该值已计算完毕,并已通过使用条件变量准备就绪,之后等待的线程将使用该值C++ 在线程之间共享变量是否存在编译器优化问题?,c++,c,multithreading,pthreads,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 <p
// 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;
}
我有点偏执,编译器的静态分析可能会确定data->spaceHit
在两个if
语句之间没有变化,从而证明使用data->spaceHit
的旧值而不是重新获取新值是合理的。我对线程和编译器优化的了解还不够,不知道这段代码是否安全。是吗
注:我在C中写了这个,并把它标记为C和C++。我在C++库中使用这个,但是因为我使用C线程API(pTrand和Win32线程),并且可以在C++库的这个部分嵌入C,所以我把它标记为C和C++。(稍后编辑:后面的答案似乎为这个问题提供了更好的答案。我将把这个答案留在这里,作为一种方法的参考,这种方法也不能回答这个问题。建议您推荐一种不同的方法。) 类型
ThreadData
本身不是易失性的
在main()
中将其实例化为“data”是易失性的。getValue()
和watchValueValue()中的指针“data”也指向“ThreadData”类型的易失性版本
虽然我喜欢第一个答案,因为它很紧凑,重写
ThreadData data; // main()
ThreadData* data; // getValue(), watchValueValue()
到
可能更好。它将确保对ThreadData成员的任何访问始终被重新读取且未进行优化。如果您向
ThreadData
添加其他字段,也会受到同样的保护。一般来说,在线程之间共享数据时,不仅存在编译器优化问题,而且还存在硬件优化问题ose线程位于不同的处理器上,可以无序执行指令
但是,pthread\u mutex\u lock
和pthread\u mutex\u unlock
函数不仅必须击败编译器缓存优化,还必须击败任何硬件重新排序优化通过执行解锁,这必须与其他线程一致。例如,在另一个处理器上不能显示锁已释放,但共享变量的更新尚未完成。因此,函数必须执行任何必要的内存障碍。如果编译器可以移动数据访问,所有这些都将无效对函数调用进行循环,或在寄存器级别缓存内容,从而破坏一致性
因此,从这个角度来看,您拥有的代码是安全的。但是,它还有其他问题。pthread\u cond\u wait
函数应该始终在重新测试变量的循环中调用,因为可能会出于任何原因被虚假唤醒
条件的信号是无状态的,因此等待的线程可能会永远阻塞。仅仅因为在getValue
输入线程中无条件调用pthread\u cond\u signal
,并不意味着watchValue
将通过等待。有可能getValue
首先执行,然后spaceHit
未设置。然后watchValue
进入互斥锁,发现spaceHit
为false,并执行一个可能不确定的等待。(讽刺的是,唯一能保存它的是虚假唤醒,因为没有循环。)
基本上,您要寻找的逻辑是一个简单的信号量:
// Consumer:
wait(data_ready_semaphore);
use(data);
// Producer:
data = produce();
signal(data_ready_semaphore);
在这种类型的交互中,我们不需要互斥,在您的watchValue
中不受保护地使用data->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);
也许您简化为示例的实际代码可以只使用信号量
p.S.pthread\u cond\u signal
也不需要在互斥锁内。它可能会调用操作系统,因此一个互斥锁保护区域只需要几条机器指令就可以保护共享变量,因为它包含信号调用,所以可能会爆炸到数百个机器周期。不允许piler在调用pthread\u cond\u wait()
或pthread\u mutex\u unlock()
时缓存data->spaceHit
的值。这两个函数都被专门称为,必须充当编译器的屏障
要使编译器成为一致性pthreads实现的一部分,它不能在您给出的情况下执行优化。要么我没有正确理解您,要么您弄错了。
main()中的数据的实例化
不是易变的
。线程函数中的td
和data
指针也没有标记为volatile
。你能澄清一下吗?我建议使用(主要标记为voltile。这将防止编译器对“数据”使用任何优化,因为它可以随时更改。只有ThreadData::spaceHit
需要标记为volatile(直接在struct
定义中)。当然,标记data
volatile也会使其所有成员都不稳定,但最好只标记ThreadData::spaceHit
volatile,因为这样你就不会有忘记它的风险(否则,您必须将每个ThreadData
实例标记为volatile,并确保所有指针都保留限定符,这很容易出错)。@chux:这一点也不错。不过,我认为constness通常是实例或别名的问题(有些需要,有些不需要)而在这种情况下,由于该特定数据成员的多线程使用,要求具有易失性
// Consumer:
wait(data_ready_semaphore);
use(data);
// Producer:
data = produce();
signal(data_ready_semaphore);
// "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);