我有一个非常简单的问题。我有简单的类型变量(如int)。我有一个进程,一个编写器线程,几个“只读”线程。我该如何声明变量?
volatile int
std::atomic<int>
int
我希望当“writer”线程修改值时,所有“读者”线程应尽快看到新值。
可以同时读取和写入变量,但我希望读者获得旧值或新值,而不是某些“中间”值。
我使用的是单CPU Xeon E5 v3机器。我不需要是可移植的,我只在这个服务器上运行代码,我用-march=native -mtune=native
编译。性能非常重要,因此除非绝对需要,否则我不想添加“同步开销”。
如果我只使用int
并且一个线程写入值,那么在另一个线程中我可能暂时没有看到“新鲜”值吗?
答案 0 :(得分:9)
只需使用std::atomic
。
请勿使用volatile
,并且不要按原样使用它;没有提供必要的同步。在一个线程中修改它并在没有同步的情况下从另一个线程访问它将给出未定义的行为。
答案 1 :(得分:5)
如果您对拥有一个或多个作者的变量有不同步的访问权限,那么您的程序就会undefined behavior。一些如何保证在写入发生时不会发生其他写入或读取。这称为synchronization。如何实现此同步取决于应用程序。
对于像这样的东西,我们有一个编写器和几个读者并使用TriviallyCopyable数据类型,那么std::atomic<>
将起作用。原子变量将确保只有一个线程可以同时访问变量。
如果您没有TriviallyCopyable类型或者您不想使用std::atomic
您还可以使用传统的std::mutex
和std::lock_guard
来控制访问
{ // enter locking scope
std::lock_guard lock(mutx); // create lock guard which locks the mutex
some_variable = some_value; // do work
} // end scope lock is destroyed and mutx is released
使用这种方法时要记住的一件重要事情是,您希望尽可能缩短// do work
部分,因为当互斥锁被锁定时,没有其他线程可以进入该部分。
另一种选择是使用std::shared_timed_mutex
(C ++ 14)或std::shared_mutex
(C ++ 17),这将允许多个读者共享互斥锁,但是当你需要编写时,你可以仍然看看互斥量并写入数据。
您不希望使用volatile
来控制jalf中this answer状态的同步:
对于共享数据的线程安全访问,我们需要保证:
- 读/写实际发生(编译器不会将值存储在寄存器中而是推迟更新主存储器直到 很久以后)
- 没有重新排序。假设我们使用
volatile
变量作为标志来指示某些数据是否准备就绪 读。在我们的代码中,我们只是在准备数据后设置标志,所以 一切都很好看。但是,如果指令被重新排序,那么该怎么办呢? 是先设定?
volatile
确实保证了第一点。它也保证不 重新排序发生在不同的volatile
读/写之间。所有volatile
内存访问将按它们的顺序进行 指定。这就是volatile
所针对的所有内容: 操纵I / O寄存器或内存映射硬件,但事实并非如此 帮助我们处理volatile
对象经常出现的多线程代码 仅用于同步对非易失性数据的访问。那些访问 仍然可以相对于volatile
进行重新排序。
与往常一样,如果您测量性能并且缺乏性能,那么您可以尝试不同的解决方案,但确保在更改后重新测量和比较。
最后,Herb Sutter在C++ and Beyond 2012 {{}}}做了一个很好的演讲,名为Atomic Weapons:
这是一个由两部分组成的演讲,内容涵盖C ++内存模型,锁和原子和围栏如何交互并映射到硬件等等。即使我们谈论C ++,其中大部分内容也适用于具有类似内存模型的Java和.NET,但不适用于C ++的所有功能(例如轻松原子)。
答案 2 :(得分:2)
我将完成以前的答案。
如前所述,由于各种原因(即使使用英特尔处理器的内存顺序约束),仅使用int或最终使用volatile也是不够的。
所以,是的,您应该使用原子类型,但需要额外考虑:原子类型保证一致访问,但如果您有可见性问题,则需要指定内存屏障(内存顺序)。
障碍将强制线程之间的可见性和一致性,在英特尔和大多数现代架构上,它将强制执行缓存同步,以便每个核心都可以看到更新。问题是,如果你不够小心,它可能会很昂贵。
可能的记忆顺序是:
因此,如果您想确保读者可以看到变量的更新,您需要使用(至少)发布内存顺序标记您的商店,并且在读者方面您需要获取内存订单(再次,至少。)否则,读者可能看不到整数的实际版本(它至少会看到一个连贯的版本,即旧版本或新版本,但不是两者的丑陋混合。)
当然,默认行为(完全一致性)也会为您提供正确的行为,但代价是大量同步。简而言之,每次添加一个屏障时,它都会强制执行缓存同步,这几乎与几次缓存未命中一样昂贵(因此在主内存中进行读/写)。
因此,简而言之,您应该将int声明为原子并使用以下代码进行存储和加载:
// Your variable
std::atomic<int> v;
// Read
x = v.load(std::memory_order_acquire);
// Write
v.store(x, std::memory_order_release);
只是为了完成,有时候(更常见的是你认为)你并不真正需要顺序一致性(甚至是部分发布/获取一致性),因为更新的可见性非常相关。在处理并发操作时,更新不是在执行写入时发生,而是在其他人看到更改时,读取旧值可能不是问题!
我强烈推荐阅读与相对论编程和RCU相关的文章,这里有一些有趣的链接:
答案 3 :(得分:1)
让我们从int
的int开始。通常,当在单处理器,单核机器上使用时,这应该足够,假设int大小等于或小于CPU字(如32位CPU上的32位int)。在这种情况下,假设正确对齐的地址字地址(高级语言应默认保证这一点),写/读操作应该是原子的。这由英特尔保证,如[1]中所述。但是,在C ++规范中,从不同线程同时读取和写入是不确定的行为。
$ 1.10
6如果其中一个修改了内存位置(1.7)而另一个访问或修改了相同的内存位置,则两个表达式评估会发生冲突。
现在volatile
。此关键字几乎禁用所有优化。这就是它被使用的原因。例如,有时在优化编译器时可以想到,只在一个线程中读取的变量在那里是常量,只需用它的初始值替换它。这解决了这些问题。但是,它不能访问变量atomic。此外,在大多数情况下,它根本就没有必要,因为使用适当的多线程工具(如互斥或内存屏障)将实现与其自身的易失性相同的效果,如[2]中所述
虽然这可能足以满足大多数用途,但还有其他操作不能保证是原子的。像增量一样。这是std::atomic
进来的时候。它定义了那些操作,就像[3]中提到的增量一样。从不同的线程读取和写入时也很明确[4]。
此外,如[5]中的答案所述,存在许多可能影响(负面)操作原子性的其他因素。从失去多个内核之间的缓存一致性到某些硬件细节是可能会改变操作执行方式的因素。
总结一下,创建std::atomic
是为了支持来自不同线程的访问,强烈建议在多线程时使用它。
[1] http://www.intel.com/Assets/PDF/manual/253668.pdf见8.1.1节。
[2] https://www.kernel.org/doc/Documentation/volatile-considered-harmful.txt
[3] http://en.cppreference.com/w/cpp/atomic/atomic/operator_arith
答案 4 :(得分:1)
不幸的是,这取决于。
当在多个线程中读取和写入变量时,可能会有2个失败。
1)撕裂。一半的数据是预变化的,一半的数据是变更后的。
2)陈旧数据。读取的数据有较旧的值。
int,volatile int和std:atomic都不会撕裂。
陈旧数据是另一个问题。但是,所有值都已存在,可以认为是正确的。
易失性。这告诉编译器既不缓存数据,也不重新排序它周围的操作。这可以通过确保线程中的所有操作都在变量之前,变量之后或之后来改善线程之间的一致性。
这意味着
volatile int x;
int y;
y =5;
x = 7;
x = 7的指令将在y = 5之后写入;
不幸的是,CPU也能够重新订购操作。这可能意味着另一个线程在y = 5
之前看到x == 7std :: atomic x;将允许保证在看到x == 7后,另一个线程会看到y == 5。 (假设其他线程没有修改y)
因此,int
,volatile int
,std::atomic<int>
的所有读数都会显示以前的有效x值。使用volatile
和atomic
可以增加值的排序。
答案 5 :(得分:0)
这是我对赏金的尝试: - 一个。上面已经给出的一般答案说&#39;使用原子&#39;。这是正确的答案。挥发性不够。 -一个。如果您不喜欢这个答案,并且您使用英特尔,并且您已经正确地对齐了int,并且您喜欢不可移植的解决方案,那么您可以使用英特尔强大的内存订购保护来消除简单的易失性。
答案 6 :(得分:0)
其他答案,即使用atomic
而非volatile
,在可移植性很重要时是正确的。如果你问这个问题,这是一个很好的问题,那就是你的实际答案,而不是,“但是,如果标准库没有提供,你可以自己实现一个无锁,无等待的数据结构“但是,如果标准库没有提供,你可以自己实现一个无锁数据结构,适用于特定的编译器和特定的架构,前提是只有一个编写器。 (另外,有人必须在标准库中实现这些原子基元。)如果我对此有误,我相信有人会通知我。
如果您绝对需要在所有平台上保证无锁的算法,您可以使用atomic_flag
构建一个算法。如果这还不够,而且你需要推出自己的数据结构,那么就可以做到这一点。
由于只有一个编写器线程,因此即使您只使用普通访问而不是锁定甚至是比较和交换,您的CPU也可能保证数据上的某些操作仍然可以原子运行。根据语言标准,这不是安全的,因为C ++必须在不支持的架构上工作,但它可以是安全的,例如,x86 CPU上的 如果您保证您正在更新的变量适合单个缓存行而不与其他任何内容共享,您可以使用__attribute__ (( aligned (x) ))
等非标准扩展来确保这一点。
同样,您的编译器可能会提供一些保证:g++
特别保证编译器不会假设volatile*
引用的内存没有更改,除非当前线程可能已更改它。每次取消引用它时,它实际上会从内存中重新读取变量。这绝不是足够的来确保线程安全,但如果另一个线程正在更新变量,它可以很方便。
一个真实的例子可能是:编写器线程维护某种指针(在其自己的缓存行上),该指针指向数据结构的一致视图,该视图将在以后的所有更新中保持有效。它使用RCU模式更新其数据,确保在更新其数据副本之后并在使指向该数据的指针全局可见之前使用发布操作(以特定于体系结构的方式实现),以便看到任何其他线程更新的指针也可以保证看到更新的数据。然后,读者对指针的当前值进行本地复制(不是volatile
),获得即使在写入器线程再次更新后也将保持有效的数据视图,并使用该数据。您希望在单个变量上使用volatile
来通知读者更新,因此即使编译器“知道”您的线程无法更改它,他们也可以看到这些更新。在这个框架中,共享数据只需要是常量,读者将使用RCU模式。这是我见过volatile
在现实世界中有用的两种方式之一(另一种方法是当你不想优化时序循环时)。
在这个方案中,还需要某种方式让程序知道何时不再使用数据结构的旧视图。如果这是读者的数量,则需要在读取指针的同时在单个操作中对该计数进行原子修改(因此获取数据结构的当前视图涉及原子CAS)。或者这可能是一个周期性的滴答,当所有线程都保证完成他们现在正在使用的数据。它可能是一代代数据结构,其中编写器通过预先分配的缓冲区旋转。
还要注意你的程序可能做的很多事情可能隐式序列化线程:那些原子硬件指令锁定处理器总线并强制其他CPU等待,那些内存栅栏可能会使你的线程停顿,或者你的线程可能正在等待从堆中分配内存的行。
答案 7 :(得分:-1)
我有简单的类型变量(比如int)。 我有一个过程,一个作家线程,几个&#34; readonly&#34;线程。怎么样 我应该声明变量吗?
volatile int 的std ::原子 INT
将std :: atomic与memory_order_relaxed一起用于商店并加载
快速,从您对问题的描述,安全。例如。
void func_fast()
{
std::atomic<int> a;
a.store(1, std::memory_order_relaxed);
}
编译为:
func_fast():
movl $1, -24(%rsp)
ret
这假设您不需要保证在更新整数之前看到任何其他数据被写入,因此不需要更慢和更复杂的同步。
如果你像这样天真地使用原子:
void func_slow()
{
std::atomic<int> b;
b = 1;
}
你得到一条没有memory_order *规范的MFENCE指令,这个指令速度较慢(100个周期以上,而裸MOV只有1或2个)。
func_slow():
movl $1, -24(%rsp)
mfence
ret
(有趣的是,英特尔对此代码使用memory_order_release和_acquire会产生相同的汇编语言。英特尔保证在使用标准MOV指令时按顺序进行写入和读取。)
答案 8 :(得分:-3)
TL; DR:如果您多次阅读,请使用std::atomic<int>
周围的互斥锁。
取决于您想要多么强大的保证。
首先volatile
是一个编译器提示,你不应指望它做一些有用的事情。
如果使用int,则可能会遇到内存别名。假设您有类似
的内容struct {
int x;
bool q;
}
根据内存中的对齐方式以及CPU和内存总线的确切实现,当页面从cpu缓存复制回ram时,写入q实际上会覆盖x。因此,除非你知道在你的int周围分配多少,否则不能保证你的编写器能够在不被其他线程覆盖的情况下编写。 即使你写了,你依赖于处理器将数据重新加载到其他内核的缓存中,因此无法保证你的其他线程会看到新值。
std::atomic<int>
基本上保证你总是会分配足够的内存,正确对齐,这样你就不会受到别名的影响。根据请求的内存顺序,您还将禁用一堆优化,例如缓存,因此一切都会稍微慢一点。
如果您多次读取var,您仍然不会获得该值。唯一的方法是在它周围加一个互斥锁来阻止编写器改变它。
最好找一个已经解决了你所遇问题的图书馆,并且已经过其他人的测试,以确保它运作良好。