如果单线程无法重新排序,则Interlocked、InterlockedAcquire和InterlockedRelea
Difference between Interlocked, InterlockedAcquire, and InterlockedRelease if single thread reordering is impossible
很可能,对于我的应用程序来说,无锁实现已经有些过头了,但无论如何,我都想研究内存障碍和无锁性,以防将来真正需要使用这些概念。
据我所知:
-
;InterlockedAcquire";函数执行原子操作,同时防止编译器将代码语句从InterlockedAcquire之后移动到Interlockedcquire之前。
-
一个";联锁释放酶";函数执行原子操作,同时防止编译器将InterlockedRelease之前的代码语句移动到InterlockedRelase之后。
-
香草";互锁的";函数执行原子操作,同时防止编译器在Interlocked调用中向任意方向移动代码语句。
我的问题是,如果一个函数的结构使得编译器无论如何都不能重新排序任何代码,因为这样做会影响单线程行为,那么Interlocked函数的任何变体之间是否存在差异,或者它们实际上都是一样的?它们之间唯一的区别是如何与代码重新排序交互吗?
对于一个更具体的例子,这里是我当前的应用程序-production()函数,它最终将成为一个使用循环缓冲区构建的多生产者、单消费者队列:
template <typename T>
class Queue {
private:
long headIndex;
long tailIndex;
T* array[MAXQUEUESIZE];
public:
Queue() {
headIndex = 0;
tailIndex = 0;
memset(array, 0, MAXQUEUESIZE*sizeof(void*);
}
~Queue() {
}
bool produce(T value) {
//1) prevents concurrent calls to produce() from causing corruption:
long indexRetVal;
long reservedIndex;
do {
reservedIndex = tailIndex;
indexRetVal = InterlockedCompareExchange64(&tailIndex, (reservedIndex + 1) % MAXQUEUESIZE, reservedIndex);
} while (indexRetVal != reservedIndex);
//2) allocates the node.
T* newValPtr = (T*) malloc(sizeof(T));
if (newValPtr == null) {
OutputDebugString("Queue: malloc returned null");
return false;
}
*newValPtr = value;
//3) prevents a concurrent call to consume from causing corruption by atomically replacing the old pointer:
T* valPtrRetVal = InterlockedCompareExchangePointer(array + reservedIndex, newValPtr, null);
//if the previous value wasn't null, then our circular buffer overflowed:
if (valPtrRetVal != null) {
OutputDebugString("Queue: circular buffer overflowed");
free(newValPtr); //as pointed out by RbMm
return false;
}
//otherwise, everything worked fine
return true;
}
};
据我所知,无论我做什么,3)都会发生在1)和2)之后,但我应该将1)更改为InterlockedRelease,因为我不在乎它是发生在2)之前还是之后,我应该让编译器来决定。
我的问题是,如果一个函数的结构使得编译器无论如何都不能对任何代码进行重新排序,因为这样做会影响单线程行为,那么Interlocked函数的任何变体之间是否存在差异,或者它们实际上都是相同的?它们之间唯一的区别是如何与代码重新排序交互吗?
您可能会将C++语句与指令混淆。你的问题不是特定于CPU的,所以你必须假装不知道CPU指令是什么样子的。
考虑这个代码:
if (a == 2)
{
b = 5;
}
现在,这里有一个不影响单个线程的代码重新排序的例子:
int c = b;
b = 5;
if (a != 2)
b = c;
这执行相同的操作,但顺序不同。它对单线程代码没有影响。但是,当然,如果另一个线程正在访问b
,它可以从该代码中看到5
的值,即使a
从来都不是2
。
因此,即使a
从不为2,它也可以从原始代码中看到5
的值
为什么,因为从单个线程的角度来看,代码的两个位执行相同的操作。除非使用具有保证线程语义的操作,否则编译器、CPU、缓存和其他平台组件都需要保留这些。
因此,最有可能的是,您认为重新排序任何代码都会影响单线程行为的想法可能是不正确的。有很多方法可以重新排序和优化代码,而不会影响单线程行为。
msdn上有一个文档解释了区别:获取和发布语义。
样品:
a++;
b++;
c++;
- 如果我们使用获取语义来增加
a
,那么其他处理器总是在b
和c
的增量之前看到a
的增量 - 如果我们使用释放语义来增加
c
,那么其他处理器总是在c
的增量之前看到a
和b
的增量 - 执行的
InterlockedXxx
例程默认具有获取和释放语义
更具体,针对4个值:
a++;
b++;
c++;
d++;
- 如果我们使用获取语义来增加
b
,那么其他处理器总是在c
和d
的增量之前看到b
的增量;顺序可以是a->b->c,d
或b->a,c,d
- 如果我们使用释放语义来增加
c
,那么其他处理器总是在c
的增量之前看到a
和b
的增量;顺序可以是a,b->c->d
或a,b,d->c
引用@antiduh:的回答
Acquire表示"只为我之后的事情担心";。新闻稿说";只有担心我面前的事情";。把这两者结合起来就是完整的记忆障碍
这三个版本都阻止编译器在函数调用中移动代码,但编译器并不是唯一进行重新排序的地方。
现代CPU具有";无序执行";甚至";投机执行";。获取和释放语义使代码编译为具有控制CPU内重新排序的标志或前缀的指令。
- 为什么在全局范围内使用"extern int a"似乎不行?
- int(c) 和 c-'0' 之间的区别。C++
- 从"int*"强制转换为"unsigned int"会丢失精度错误
- 为什么野牛仍在使用"int yylex(void)",却找不到"int yylex(YYS
- 有符号的int和int-有没有一种方法可以在C++中区分它们
- 请解释这句话(cout<<1+int((a<b)^((b-a)&1) )<<endl
- 是否可以从int转换为enum类类型
- 不能在初始值设定项列表中将非常量表达式从类型 'int' 缩小到'unsigned long long'
- 向量 <int> a {N, 0} 和 int arr a[N] = {0} 的时间复杂度有什么区别
- 'short int'持有的值溢出,但"自动"不会溢出?
- 如何在C++中将一个无符号的 int 转换为两个无符号的短裤?
- 调用'begin(int [n])'没有匹配函数
- 没有显式声明的int[]中的foreach
- 在c++中访问int到类对象的映射时出错
- 为什么我无法更改"set<set>"循环中的值<int>
- 长 长 int 不要 长 int 好
- C++程序在循环后给出奇怪的int值
- 如何计算数据类型的范围,例如int
- 如果"new int"返回"int*",那么为什么"new int[n]"不返回"int**"?
- 如何在cpp.中使用协议缓冲区存储大缓冲区/数组(char/int)