快速提问?这条线在 C++ 和 Java 中是原子的吗?
class foo {
bool test() {
// Is this line atomic?
return a==1 ? 1 : 0;
}
int a;
}
如果有多个线程访问该行,我们最终可以先检查 a==1,然后更新 a,然后返回,对吗?
补充:我没有完成课程,当然还有其他部分更新了...
不,对于 C++ 和 Java。
在 Java 中,您需要以相同的方式创建您的方法synchronized
并保护其他用途a
。确保在所有情况下都在同一个对象上进行同步。
在 C++ 中,您需要使用std::mutex
to protect a
,可能std::lock_guard
用于确保在函数结束时正确解锁互斥锁。
return a==1 ? 1 : 0;
是一种简单的写作方式
if(a == 1)
return 1;
else
return 0;
我没有看到任何用于更新 a 的代码。但我想你可以弄清楚。
不管有没有写,在C++中读取一个非原子类型的值都不是原子操作。如果没有写入,那么您可能不在乎它是否是原子的;如果其他一些线程可能正在修改该值,那么您当然会关心。
正确的说法很简单:不!(适用于 Java 和 C++)
一个不太正确但更实际的答案是:从技术上讲,这不是原子的,但在大多数主流架构上,至少对于 C++ 来说是这样。
您发布的代码中没有任何内容被修改,该变量仅经过测试。因此,代码通常会导致访问该内存位置的单个TEST
(或类似)指令,顺便说一下,是原子的。该指令将读取一个高速缓存行,并且在相应的位置中将有一个明确定义的值,无论它可能是什么。
但是,这是偶然/偶然的,不是您可以依赖的。
当单个其他线程写入该值时,它通常甚至会起作用 - 再次,偶然地/偶然地。为此,CPU 获取一个高速缓存行,覆盖高速缓存行内相应地址的位置,并将整个高速缓存行写回 RAM。当您测试变量时,您会获取一个包含旧值或新值(两者之间没有任何内容)的缓存行。没有任何形式的保证,但您仍然可以认为这是“原子的”。
当多个线程同时修改该变量时(不是问题的一部分),情况要复杂得多。为了使其正常工作,您需要使用 C++11 中的某些东西<atomic>
,或者使用原子内在函数或类似的东西。否则,非常不清楚会发生什么,以及操作的结果可能是什么——一个线程可能会读取该值,将其递增并将其写回,但另一个线程可能会在修改后的值被写回之前读取原始值。
在所有当前平台上,这或多或少肯定会以糟糕的方式结束。
不,它不是原子的(通常),尽管它可以在某些体系结构中(例如,在 C++ 中,在 intel 中,如果整数是对齐的,除非你强迫它不对齐)。
考虑这三个线程:
// thread one: // thread two: //thread three
while (true) while (true) while (a) ;
a = 0xFFFF0000; a = 0x0000FFFF;
如果写入a
不是原子的(例如,intel ifa
是未对齐的,并且为了讨论,两个连续高速缓存行中的每一行都有 16 位)。现在,虽然第三个线程似乎永远无法退出循环(两个可能的值a
都是非零),但事实是分配不是原子的,线程二可以将高 16 位更新为 0,并且线程三可以在线程二获得时间完成更新之前将低 16 位读取为 0,然后退出循环。
整个条件与问题无关,因为返回值是线程本地的。
您的问题可以改写为:是声明:
a == 1
原子与否?不,它不是原子的,您应该使用 std::atomic 作为 a 或在某种锁定下检查该条件。在这种情况下,如果整个三元运算符原子与否无关紧要,因为它不会改变任何东西。如果你的意思是在你的问题中如果在这段代码中:
bool flag = somefoo.test();
标志与 a == 1 一致,它肯定不会,并且如果您的问题中的整个三元运算符是原子的,则无关紧要。
不,它仍然是一个测试,然后是一组,然后是一个返回。
是的,多线程将是一个问题。
这只是语法糖。
考虑以下代码:
bool done = false;
void Thread1() {
while (!done) {
do_something_useful_in_a_loop_1();
}
do_thread1_cleanup();
}
void Thread2() {
do_something_useful_2();
done = true;
do_thread2_cleanup();
}
这两个线程之间的同步是使用一个布尔变量 done 完成的。这是同步两个线程的错误方法。
在 x86 上,最大的问题是编译时优化。
编译器可以将 do_something_useful_2() 的部分代码移到“done = true”下方。编译器可以将 do_thread2_cleanup() 的部分代码移到“done = true”之上。如果 do_something_useful_in_a_loop_1() 没有修改“done”,编译器可能会通过以下方式重写 Thread1:
if (!done) {
while(true) {
do_something_useful_in_a_loop_1();
}
}
do_thread1_cleanup();
所以 Thread1 永远不会退出。
在 x86 以外的架构上,缓存效果或乱序指令执行可能会导致其他微妙的问题。
大多数种族检测器都会检测到这样的种族。
此外,大多数动态竞争检测器将报告旨在与此 bool 同步的内存访问的数据竞争
(即在 do_something_useful_2() 和 do_thread1_cleanup() 之间)
要解决此类竞争,您需要使用编译器和/或内存屏障(如果您不是专家 - 只需使用锁)。
这里有很多好的答案,但没有一个提到在 Java 中需要标记a
为volatile
.
如果没有采用其他同步方法,这一点尤其a
重要,但其他线程可以更新. 否则,您可能正在读取a
.