考虑以下C代码片段:
int flag = 0;
/* Assume that the functions lock_helper, unlock_helper implement enter/leave in
* a global mutex and thread_start_helper simply runs the function in separate
* operating-system threads */
void worker1()
{
/* Long-running job here */
lock_helper();
if (!flag)
flag = 1;
unlock_helper();
}
void worker2()
{
/* Another long-running job here */
lock_helper();
if (!flag)
flag = 2;
unlock_helper();
}
int main(int argc, char **argv)
{
thread_start_helper(&worker1);
thread_start_helper(&worker2);
do
{
/* doing something */
} while (!flag);
/* do something with 'flag' */
}
问题:
主线程(和它)的'flag'是否总是为0? 由于某些编译器优化,它会陷入do / while循环中?
'volatile'修饰符会有什么不同吗?
如果答案是“取决于编译器提供的功能”,那么有没有 我可以使用配置脚本来检查这个“功能” 编译时
答案 0 :(得分:2)
代码可能按原样运行,但有点脆弱。首先,它取决于对正在使用的处理器上的flag
的读取和写入(并且flag
的对齐就足够了)。
我建议使用读锁来读取flag
的值,或使用您正在使用的任何线程库的功能使flag
正确原子。
答案 1 :(得分:0)
有可能在线程之前执行while ...你必须等待执行线程之前,使用pthread_join()
答案 2 :(得分:0)
由于您可以假设对齐int
的加载是原子操作,因此您的代码唯一的危险就是优化器:允许您的编译器优化除{{1}的第一次读取之外的所有内容在flag
内,我。即将您的代码转换为
main()
有两种方法可以确保不会发生这种情况:1。使int main(int argc, char **argv)
{
thread_start_helper(&worker1);
thread_start_helper(&worker2);
/* doing something */
if(!flag) {
while(1) /* doing something */
}
//This point is unreachable and the following can be optimized away entirely.
/* do something with 'flag' */
}
易变,这是一个坏主意,因为它包含了相当多的不需要的开销,并且2.介绍必要的记忆障碍。由于读取flag
的原子性以及您只想在更改后解释int
的值这一事实,您应该能够在循环条件之前只使用编译器屏障像这样:
flag
这里使用的int main(int argc, char **argv)
{
thread_start_helper(&worker1);
thread_start_helper(&worker2);
do
{
/* doing something */
barrier();
} while(!flag)
/* do something with 'flag' */
}
非常轻巧,是所有障碍中最便宜的。
如果要分析在引发barrier()
之前写入的任何其他数据,这还不够,因为您仍可能从内存中加载过时数据(因为CPU决定预取该值)。有关记忆围栏,其必要性及其用途的全面讨论,请参阅https://www.kernel.org/doc/Documentation/memory-barriers.txt
最后,您应该知道,在flag
循环退出后,其他编写者线程可能会随时修改flag
。因此,您应立即将其值复制到阴影变量,如下所示:
do{}while()