8.1.2总线锁定
Intel 64和IA-32处理器提供LOCK#信号,该信号在某些关键存储器操作期间自动置位,以锁定系统总线或等效链路.当该输出信号被断言时,来自其他处理器或总线代理的用于控制总线的请求被阻止.软件可以指定在遵循LOCK语义的其他情况下将LOCK前缀添加到指令之前.
它来自英特尔手册,第3卷
听起来内存上的原子操作将直接在内存(RAM)上执行.我很困惑,因为当我分析装配输出时,我看到"没什么特别的".基本上,生成的汇编输出std::atomic<int> X; X.load()只会产生"额外"的影响.但是,它负责正确的内存排序,而不是原子性.如果我理解得X.store(2)恰到好处mov [somewhere], $2.就这样.它似乎没有"跳过"缓存.我知道将对齐(例如int)移动到内存是原子的.但是,我很困惑.
所以,我提出了疑问,但主要问题是:
当从原子函数指针调用函数时,例如:
#include <atomic>
#include <type_traits>
int func0(){ return 0; }
using func_type = std::add_pointer<int()>::type;
std::atomic<func_type> f = { func0 };
int main(){
f();
}
Run Code Online (Sandbox Code Playgroud)
gcc 根本不抱怨,而 clang 和 msvc 在调用方面有问题f():
Clang 还指定了可能的候选调用:
operator __pointer_type() const noexceptoperator __pointer_type() const volatile noexcept看起来这种波动性的差异对于 clang 和 msvc 来说是令人困惑的,但对于 gcc 却不是。
f()当 call从改为 时f.load()(),代码可以在所有上述编译器中运行。这更令人困惑,因为据说load()和都有和重载 - 如果隐式转换不起作用,我预计也不会起作用。隐式转换(与成员调用)中的规则是否有所不同?operator T()constconst volatileload()
那么,gcc 接受该代码是错误的吗?clang和msvc错误会报错吗?还有其他错误或正确的组合吗?
这主要是一个理论问题,但如果有更好的方法来拥有原子函数指针,我想知道。
什么时候可以保证64位写入是原子的,在基于Intel x86的平台上用C编程时(特别是使用英特尔编译器运行MacOSX 10.4的基于Intel的Mac)?例如:
unsigned long long int y;
y = 0xfedcba87654321ULL;
/* ... a bunch of other time-consuming stuff happens... */
y = 0x12345678abcdefULL;
Run Code Online (Sandbox Code Playgroud)
如果另一个线程在y的第一次赋值完成后检查y的值,我想确保它看到值0xfedcba87654321或值0x12345678abcdef,而不是它们的某些混合.我想这样做没有任何锁定,如果可能的话没有任何额外的代码.我希望在使用64位编译器(64位Intel编译器)时,在能够支持64位代码的操作系统(MacOSX 10.4)上,这些64位写入将是原子的.这总是如此吗?
在我工作的程序中,我有很多代码如下:
pthread_mutex_lock( &frame->mutex );
frame->variable = variable;
pthread_mutex_unlock( &frame->mutex );
Run Code Online (Sandbox Code Playgroud)
如果中间指令可以用原子存储替换,这显然是浪费CPU周期.我知道gcc非常有能力,但是我没能找到关于这种简单的线程安全原子操作的文档.我如何用原子操作替换这组代码?
(我知道简单商店在理论上应该是原子的,但我不希望希望优化器在这个过程的某个时刻不会搞砸他们的原子性.)
澄清:我不需要它们是严格的原子; 这些变量仅用于线程同步.也就是说,线程B读取值,检查它是否正确,如果不正确,它会休眠.因此,即使线程A更新了值并且线程B没有意识到它的更新,这也不是问题,因为这只是意味着线程B在它不需要时就会休眠,并且当它被唤醒时,值将会是对的.
x86和其他体系结构提供了特殊的原子指令(lock,cmpxchg等),允许您编写"无锁"数据结构.但随着越来越多的内核被添加,似乎这些指令实际上必须在幕后进行的工作将会增长(至少是为了保持缓存一致性?).如果原子添加在双核系统上今天需要大约100个周期,那么未来的80多个核心机器上可能需要更长的时间吗?如果您要将代码编写为最后一个,那么即使它们今天变慢,使用锁实际上是否更好?
我正在寻找一种方法来实现支持单个生产者和多个消费者的无锁队列数据结构.我看过Maged Michael和Michael Scott(1996)的经典方法,但他们的版本使用链表.我想要一个使用有界循环缓冲区的实现.什么东西使用原子变量?
另外,我不确定为什么这些经典方法是为需要大量动态内存管理的链表设计的.在多线程程序中,所有内存管理例程都是序列化的.我们不是通过将它们与动态数据结构结合使用来破坏无锁方法的好处吗?
我试图在英特尔64位架构上使用pthread库在C/C++中编写代码.
谢谢Shirish
AtomicXXX.lazySet(value)方法在大多数JMM推理中使用的前沿边缘是什么意思?javadocs是纯粹的,Sun bug 6275329声明:
语义是保证写入不会与任何先前的写入重新排序,但可以与后续操作重新排序(或者等效地,可能对其他线程不可见),直到发生一些其他易失性写入或同步动作).
但这不是关于HB边缘的推理,所以它让我感到困惑.这是否意味着什么lazySet()语义不能用HB边缘表示?
更新:我会尝试将我的问题具体化.我可以在以下场景中使用普通的volatile字段:
//thread 1: producer
...fill some data structure
myVolatileFlag = 1;
//thread 2: consumer
while(myVolatileFlag!=1){
//spin-wait
}
...use data structure...
Run Code Online (Sandbox Code Playgroud)
在这种情况下,在消费者中使用"数据结构"是正确的,因为易失性标志写入读取使得HB边缘,从而保证生产者对"数据结构"的所有写入都将完成,并且消费者可以看到.但是如果我在这种情况下使用AtomicInteger.lazySet/get而不是volatile写/读怎么办?
//thread 1: producer
...fill some data structure
myAtomicFlag.lazySet(1);
//thread 2: consumer
while(myAtomicFlag.get()!=1){
//spin-wait
}
...use data structure...
Run Code Online (Sandbox Code Playgroud)
它仍然是正确的吗?消费者线程中的"数据结构"值可见性真的可以吗?
它不是"来自空中"的问题 - 我在LMAX Disruptor代码中看到了这种方法正好在这种情况下,我不明白如何证明它是正确的......
std::atomic功能,诸如store和load采取一个std::memory_order参数.可以在运行时确定参数,就像任何其他函数参数一样.但是,实际值可能会影响编译期间代码的优化.考虑以下:
std::atomic<int> ai1, ai2;
int value = whatever;
void foo() {
std::memory_order memOrd = getMemoryOrder();
register int v = value; // load value from memory
ai1.store(v, memOrd); // dependency on v's value
ai2.store(1, memOrd); // no dependency. could this be move up?
}
Run Code Online (Sandbox Code Playgroud)
如果memOrd碰巧memory_order_relaxed,第二个商店可以安全地移动到第一个商店前面.这将在加载value和使用它之间增加一些额外的工作,这可能会阻止其他需要的停顿.但是,如果memOrd是memory_order_seq_cst,则不应该允许切换存储,因为如果设置为1 ai1,value则某些其他线程可能指望已经设置为ai2.
我想知道为什么内存顺序被定义为运行时参数而不是编译时间.在决定最佳内存操作语义之前,有人在运行时检查环境是否有任何理由?
我在我的库中使用std :: atomic和自定义类.一切都适用于MSVC,但现在我试图让它在macOS上运行,我得到一个链接器错误:
体系结构x86_64的未定义符号:"_ _ atomic_store",引自:main.o中的_main
我已经创建了一些测试代码来复制它
#include <iostream>
#include <atomic>
using namespace std;
class Vec {
public:
int x, y, z;
Vec() { x = y = z = 0; }
};
std::atomic<Vec> x;
int main()
{
Vec a;
x = a;
cin.get();
return 0;
}
Run Code Online (Sandbox Code Playgroud)
当然这个例子没有多大意义,但它是我能想到的最短的.它确实在VS2012中运行,但不在xcode中运行(给我上面显示的链接器错误).
发生什么了?我在这里滥用std :: atomic吗?我正在处理的库是多线程的,用于音频处理.因此,如果我没有以正确的方式使用std :: atomic,那么它并没有真正显示,因为性能非常好,而且我没有任何线程问题.或者xcode可能缺少什么?
更新:
我检查了andrey的答案,因为它有最多的信息,尽管所有3个答案都很有用.我显然不是这方面的专家,但似乎VS2012超出了C++ 11中应该实现的范围.
那么如何离开这里?我看到了一些选择.
在提议的boost :: concurrent_unordered_map的线程清理过程中出现了一些非常奇怪的东西,并在这篇博客文章中进行了叙述.简而言之,bucket_type如下所示:
struct bucket_type_impl
{
spinlock<unsigned char> lock; // = 2 if you need to reload the bucket list
atomic<unsigned> count; // count is used items in there
std::vector<item_type, item_type_allocator> items;
bucket_type_impl() : count(0), items(0) { }
...
Run Code Online (Sandbox Code Playgroud)
然而线程消毒者声称在bucket_type的构造和它的第一次使用之间存在竞争,特别是当加载计数原子时.事实证明,如果你通过它的构造函数初始化std :: atomic <>,那么初始化不是原子的,因此内存位置不是原子释放的,因此对其他线程不可见,这是违反直觉的,因为它是一个原子,并且大多数原子操作默认为memory_order_seq_cst.因此,您必须在构造之后显式执行发布存储,以使用其他线程可见的值初始化原子.
是否有一些非常迫切的原因,为什么std :: atomic与值消耗构造函数不会使用发布语义初始化自己?如果没有,我认为这是一个库缺陷.
编辑:乔纳森的回答是,为什么在历史上较好,但ecatmur的回答环节对此事阿拉斯泰尔的缺陷报告,以及它是如何通过简单地添加注释说建设没有提供可视性,其他线程关闭.因此,我将奖励ecatmur.感谢所有回复的人,我认为要求一个额外的构造函数的方式很明显,它至少会在文档中脱颖而出,值得使用构造函数.
编辑2:我最终将此作为C++语言中的缺陷与委员会一起提出,并且并发部分主持人Hans Boehm认为这不是问题,原因如下:
2014年没有现有的C++编译器将消费视为与获取不同.正如您将永远不会,在现实世界的代码中,将原子传递给另一个线程而不经过一些释放/获取,原子的初始化将使用原子对所有线程可见.我觉得这很好,直到编译器赶上来,在此之前,Thread Sanitiser会对此发出警告.
如果你做不匹配的消耗,获取释放像我(我使用的版本,里面锁/消耗,外锁原子,以推测避免释放获取自旋锁的地方是不必要的),那么你是一个大足够的男孩知道你必须在施工后手动储存释放原子.这可能是一个公平的观点.