dan*_*dan 12 c++ multithreading atomic volatile
所以我知道C++中没有什么是原子的.但我想弄清楚我是否有任何"伪原子"假设.原因是我想避免在一些简单的情况下使用互斥锁,我只需要非常弱的保证.
1)假设我有全局定义的volatile bool b,最初我设置为true.然后我启动一个执行循环的线程
while(b) doSomething();
Run Code Online (Sandbox Code Playgroud)
同时,在另一个线程中,我执行b = true.
我可以假设第一个线程将继续执行吗?换句话说,如果b开始为true,并且第一个线程在第二个线程分配b = true的同时检查b的值,我可以假设第一个线程将b的值读为true吗?或者有可能在赋值的某个中间点b = true,b的值可能被读为false?
2)现在假设b最初是假的.然后第一个线程执行
bool b1=b;
bool b2=b;
if(b1 && !b2) bad();
Run Code Online (Sandbox Code Playgroud)
而第二个线程执行b = true.我可以假设bad()永远不会被调用吗?
3)int或其他内置类型怎么样:假设我有volatile int i,最初(比如说)7,然后我指定i = 7.我可以假设,在此操作期间的任何时间,从任何线程,i的值将等于7?
4)我有volatile int i = 7,然后我从某个线程执行i ++,所有其他线程只读取i的值.除了7或8之外,我可以假设我在任何线程中都没有任何价值吗?
5)我有一个volatile int i,从一个执行i = 7的线程,从另一个执行i = 8.之后,我保证是7或8(或者我选择分配的两个值)?
Ste*_*sop 14
标准C++中没有线程,并且Threads不能实现为库.
因此,该标准对使用线程的程序的行为没有任何意义.您必须查看线程实现提供的任何其他保证.
也就是说,在我使用的线程实现中:
(1)是的,您可以假设不相关的值不会写入变量.否则整个内存模型就会消失.但要小心,当你说"另一个线程"从未设置b
为假时,这意味着任何地方,永远.如果是这样,那么写入可能会在循环期间重新排序.
(2)不,编译器可以将分配重新排序为b1和b2,因此b1可能最终为真,而b2为假.在这么简单的情况下,我不知道它为什么要重新排序,但在更复杂的情况下,可能有很好的理由.
[编辑:oops,当我回答时(2)我忘记了b是不稳定的.从一个volatile变量读取不会被重新排序,对不起,所以在典型的线程实现上是这样的(如果有任何这样的事情),你可以假设你最终不会以b1为真,而b2为假.
(3)与1.相同volatile
通常与线程无关.但是,在某些实现(Windows)中它非常令人兴奋,并且实际上可能意味着内存障碍.
(4)在int
写入是原子的架构上是,虽然volatile
与它无关.也可以看看...
(5)仔细检查文档.可能是的,并且volatile再次无关紧要,因为在几乎所有架构中,int
写入都是原子的.但是如果int
写入不是原子的,那么就没有(前一个问题没有),即使它是不稳定的,你原则上可以获得不同的值.但是,鉴于这些值为7和8,我们正在讨论一个非常奇怪的架构,其中包含要在两个阶段写入的相关位,但是使用不同的值可以更合理地获得部分写入.
对于一个更合理的例子,假设出于一些奇怪的原因,你在一个平台上只有8位int,其中只有8位写入是原子的.奇怪,但合法,因为int
必须至少16位,你可以看到它是如何产生的.进一步假设你的初始值是255.那么增量可以合法地实现为:
一个只读线程,它在第三步和第四步之间中断递增线程,可以看到值511.如果写入是另一个顺序,它可以看到0.
如果一个线程写入255,另一个线程同时写入256,并且写入交错,则永久保留不一致的值.许多架构都不可能,但要知道这不会发生,你至少需要知道一些架构.C++标准中没有任何内容禁止它,因为C++标准谈到执行被信号中断,但是否则没有执行的概念被程序的另一部分中断,也没有并发执行的概念.这就是为什么线程不仅仅是另一个库 - 添加线程从根本上改变了C++执行模型.它要求实现以不同的方式执行操作,因为您最终会发现是否例如在gcc下使用线程而忘记指定-pthreads
.
在对齐 int
写入是原子的平台上也可能发生同样的情况,但是int
允许未对齐写入而不是原子写入.例如,对于x86上的IIRC,如果未对齐int
写入超过高速缓存行边界,则不保证原子写入.int
由于这个原因,x86编译器不会错误地对齐声明的变量.但如果你玩结构包装的游戏,你可能会引发一个例子.
所以:几乎任何实现都会为您提供所需的保证,但可能会以相当复杂的方式完成.
一般来说,我发现不值得尝试依赖特定于平台的内存访问保证,我不完全理解,以避免互斥.使用互斥锁,如果速度太慢,请使用由真正了解架构和编译器的人编写的高质量无锁结构(或实现一个设计).它可能是正确的,并且正确性可能会优于我自己创造的任何东西.
大多数答案都正确地解决了您将要遇到的CPU内存排序问题,但没有一个能够详细说明编译器如何通过以破坏您的假设的方式重新排序代码来挫败您的意图.
考虑一下这篇文章的一个例子:
volatile int ready;
int message[100];
void foo(int i)
{
message[i/10] = 42;
ready = 1;
}
Run Code Online (Sandbox Code Playgroud)
在-O2
以上,GCC和英特尔C/C++的最新版本(不知道VC++)会做实体店ready
第一,所以它可以与计算重叠i/10
(volatile
不救你!):
leaq _message(%rip), %rax
movl $1, _ready(%rip) ; <-- whoa Nelly!
movq %rsp, %rbp
sarl $2, %edx
subl %edi, %edx
movslq %edx,%rdx
movl $42, (%rax,%rdx,4)
Run Code Online (Sandbox Code Playgroud)
这不是一个错误,它是利用CPU流水线的优化器.如果ready
在访问内容之前另一个线程正在等待,message
那么你就会有一个令人讨厌且模糊不清的比赛.
采用编译器障碍以确保您的意图得到尊重.还利用86的相对强排序的例子是释放/消耗德米特里Vyukov的单生产者单消费者队列中找到包装贴在这里:
// load with 'consume' (data-dependent) memory ordering
// NOTE: x86 specific, other platforms may need additional memory barriers
template<typename T>
T load_consume(T const* addr)
{
T v = *const_cast<T const volatile*>(addr);
__asm__ __volatile__ ("" ::: "memory"); // compiler barrier
return v;
}
// store with 'release' memory ordering
// NOTE: x86 specific, other platforms may need additional memory barriers
template<typename T>
void store_release(T* addr, T v)
{
__asm__ __volatile__ ("" ::: "memory"); // compiler barrier
*const_cast<T volatile*>(addr) = v;
}
Run Code Online (Sandbox Code Playgroud)
我建议如果您打算进入并发内存访问领域,请使用一个可以为您处理这些细节的库.虽然我们都在等待n2145并std::atomic
查看Thread Building Blocks tbb::atomic
或即将推出的boost::atomic
.
除了正确性,这些库可以简化您的代码并澄清您的意图:
// thread 1
std::atomic<int> foo; // or tbb::atomic, boost::atomic, etc
foo.store(1, std::memory_order_release);
// thread 2
int tmp = foo.load(std::memory_order_acquire);
Run Code Online (Sandbox Code Playgroud)
使用显式内存排序,foo
线程间的关系很明确.
归档时间: |
|
查看次数: |
3830 次 |
最近记录: |