考虑以下示例。目标是使用两个线程,一个用于“计算”一个值,另一个用于消耗和使用计算值(我试图简化这个)。计算线程通过使用条件变量向另一个线程发信号通知该值已经计算并准备就绪,之后等待的线程消耗该值。
// 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->spaceHit
在两个if
语句之间没有变化,从而证明使用data->spaceHit
的旧值而不是重新获得新价值。我对线程和编译器优化知之甚少,不知道这段代码是否安全。是吗?
注意:我在C中编写了这个,并将其标记为C和C ++。我在C ++库中使用它,但由于我使用的是C线程API(pthreads和Win32线程),并且可以选择在C ++库的这一部分中嵌入C,我将其标记为C和C ++
答案 0 :(得分:9)
不,编译器不允许在data->spaceHit
或pthread_cond_wait()
的调用中缓存pthread_mutex_unlock()
的值。这些都被特别称为"functions [which] synchronize memory with respect to other threads",它必须充当编译器障碍。
要使编译器成为符合标准的pthreads实现的一部分,它必须在您给出的情况下不执行该优化。
答案 1 :(得分:6)
一般来说,线程之间共享数据不仅存在编译器优化问题,而且当这些线程位于可以无序执行指令的不同处理器上时会出现硬件优化问题。
但是,pthread_mutex_lock
和pthread_mutex_unlock
函数不仅要击败编译器缓存优化,还要击败任何硬件重新排序优化。如果线程A准备一些共享数据,然后通过执行解锁来“发布”它,那么它必须与其他线程一致。例如,它不会出现在另一个处理器上释放锁,但共享变量的更新尚未完成。因此,函数必须执行任何必要的内存屏障。如果编译器可以围绕对函数的调用移动数据访问,或者在寄存器级别缓存事物以便一致性被破坏,那么所有这些都是徒劳的。
因此,从这个角度来看,您拥有的代码是安全的。但是,它还有其他问题。应始终在循环中调用pthread_cond_wait
函数,该循环重新测试变量,因为任何原因都可能出现虚假唤醒。
条件的信号是无状态的,因此等待的线程可以永久阻塞。仅仅因为您在pthread_cond_signal
输入线程中无条件地调用getValue
并不意味着watchValue
将通过等待。 getValue
可能先执行,spaceHit
未设置。然后watchValue
进入互斥锁,看到spaceHit
为假并执行可能无限期的等待。 (唯一可以拯救它的是诡异的唤醒,具有讽刺意味的是,因为没有循环。)
基本上你似乎在寻找的逻辑是一个简单的信号量:
// Consumer:
wait(data_ready_semaphore);
use(data);
// Producer:
data = produce();
signal(data_ready_semaphore);
在这种交互方式中,我们不需要互斥量,这是通过data->spaceHit
中watchValue
的无保护使用暗示的。更具体地说,使用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_cond_signal
也不必在互斥锁内。它可能会调用操作系统,因此只需要保护共享变量的互斥保护区域只需要少量机器指令就可以吹掉数百个机器周期,因为它包含信令调用。
答案 2 :(得分:-1)
(稍后编辑:看起来后续的答案为这个查询提供了更好的答案。我会在这里留下这个答案作为不回答问题的方法的参考。建议你应该推荐一种不同的方法。)
类型ThreadData
本身不易变。
在main()
中将其实例化为“数据”是不稳定的。 getValue()
和watchValueValue()中的指针'data'也指向'ThreadData'类型的易失版本。
虽然我喜欢它的紧张性的第一个答案,但重写
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
添加其他字段,则同样受到保护。