比较结果的原子交换值
Posted
技术标签:
【中文标题】比较结果的原子交换值【英文标题】:Atomically exchange value on result of comparison 【发布时间】:2011-10-29 08:02:42 【问题描述】:我有一个非常简单的操作需要原子完成:
if (a > b)
b = a
其中 a 和 b 是整数
编辑:并且 a 是本地的。
在 C# 中有没有快速的方法来做到这一点?如果可能的话,我想避免手动锁定。我查看了 Interlocked.CompareExchange,但据我了解,这仅测试相等性。
谢谢!
【问题讨论】:
强制性旧新事物链接:blogs.msdn.com/b/oldnewthing/archive/2004/09/15/229915.aspx @vcsjones:有趣的文章,谢谢。但是,在我的情况下, b 仅在上述情况下才被写入。我认为 Interlocked 类型的方法在这里是合适的。 @dcrooney - 互锁可能是合适的,您会看到用于解决它的解决方案(循环中的 CompareExchange)是 Henning Makholm 的答案。 类似于***.com/questions/3731088/… @Brian Gideon :看起来不太相似,这是我们有一个局部变量,没有多个线程访问它 【参考方案1】:规范的方法是在循环中使用互锁的比较交换:
int oldvalue, newvalue ;
do
oldvalue = b ; // you'll want to force this to be a volatile read somehow
if( a > oldvalue )
newvalue = a ;
else
break ;
while( interlocked replace oldvalue with newvalue in b does NOT succeed );
(伪代码,因为我懒得查找在 C# 中进行互锁交换的正确方法)。
如您所见,除非您有最重要的效率问题,否则使用普通的旧互斥锁要简单得多,也更易读。
编辑: 这假定a
是一个局部变量或至少不受异步写入的影响。 a
和 b
都可以在你背后修改,那么就没有无锁的方式来原子地进行更新。 (感谢 silev 指出这一点)。
【讨论】:
但这将是手动锁定和很多带有循环的东西,矫枉过正,你怎么看? 我将“避免手动锁定”理解为他不想使用lock
关键字或显式互斥锁。如果这是边界条件(我并不是真的赞同它,应该很清楚),那么使用循环是不可避免的,至少在 x86 锁定模型中是这样。
试想一下 THREAD#1:读取 'a' 的值,假设行后 10 if(a > oldvalue) ==> 线程切换 ==> THREAD#2:将 -1 分配给 'a' ==> 线程切换 ==> THREAD#1:尝试将 'a' 分配给 newValue == 结果将是 -1 但最初 THREAD#1 读取为 10
@sllev:进行几次循环迭代(甚至 100 次)比等待临界区甚至只是尝试进入临界区要快。
@vcsjones :绝对(我了解 lock() 的底层内容),但还有其他选择吗?【参考方案2】:
Henning is correct。我将提供与 C# 相关的详细信息。该模式可以用以下函数进行泛化。
public static T InterlockedOperation<T>(ref T location, T operand)
T initial, computed;
do
initial = location;
computed = op(initial, operand); // where op is replaced with a specific implementation
while (Interlocked.CompareExchange(ref location, computed, initial) != initial);
return computed;
在您的具体情况下,我们可以像这样定义InterlockedGreaterThanExchange
函数。
public static int InterlockedGreaterThanExchange(ref int location, int value)
int initial, computed;
do
initial = location;
computed = value > initial ? value : initial;
while (Interlocked.CompareExchange(ref location, computed, initial) != initial);
return computed;
【讨论】:
MemoryBarrier 仅在内存排序较弱的多处理器系统上需要(例如,使用多个 Intel Itanium 处理器的系统)。对于大多数用途,C# lock 语句、Visual Basic SyncLock 语句或 Monitor 类提供了更简单的数据同步方法。 msdn.microsoft.com/en-us/library/… 所以你认为它会比 lock() 更简单、更快? 内存屏障肯定是需要的。它对Thread.MemoryBarrier
的调用可以省略,因为Interlocked.CompareExchange
已经生成了一个防止location
的读取被提升(通过优化)在循环之外。你是对的,x86 硬件有严格的保证,但是 CLI(通过 ECMA 规范)的保证很弱,这意味着优化可以在 JIT 级别进行。您必须为较弱的模型编写代码。这就是为什么 Thread.MemoryBarrier
等首先存在的原因。
我认为一个普通的旧 lock
会更简单,这就是为什么我赞成你的答案,但是带有 CAS 操作的循环可能会更快,尤其是在具有许多内核的高并发系统中。跨度>
【参考方案3】:
没有这样的原子操作以真正的原子方式执行比较和赋值。在 C# 中,真正的原子操作可以是 Interlocked
操作,如 CompareExchenge()
和 Exchange()
以交换两个整数值(32 位用于 32 位架构,64 位用于 64 位处理器架构)。
显然简单的赋值a = b
是两个操作——读和写。所以不可能在一个原子步骤中进行比较+分配......在任何其他语言/体系结构中是否有可能,我的意思是真正的原子? ;)
编辑:原始问题并未表明“a”是局部变量... uhhhggg 这么小的修正...无论如何我都会留下我的答案,因为我相信真正的原子操作的本质而不是某种具有原子野心的“技巧”
总结一下:
我们不能做像单个原子操作这样的操作 只有一种方法是使用锁定机制对其进行同步(假设原始问题并未说明a
是本地的,因此a
和b
都可能被其他线程访问
object lockObject = new object();
int a = 10;
int b = 5;
lock (lockObject)
if (a > b)
b = a
【讨论】:
我对这个答案持中立态度,因为它是正确的,尽管自以为是,但它给出了一个不错的理由。但我讨厌看到它得到反对票,所以赞成0
。【参考方案4】:
这些是我在阅读其他答案后提出的一些优化“食谱”。与问题没有直接关系,但在此处添加,因为这是相关搜索的位置。试图将其保留为 a > b
或 a >= b
以匹配原始问题。并使它们保持一般性,以免使描述偏离其他相关问题。两者都可以封装到方法中。
优化 - 单调b
如果这是对b
或b
执行的唯一操作,否则单调递增,您可以优化互锁分配和重试其中a > b == false
:
int initial;
do
initial = b;
while ((a > initial) && Interlocked.CompareExchange(ref b, a, initial) != initial);
如果a > b == false
在任何重试后都不会是true
(b
只会越来越大)并且可以跳过交换,因为b
不会受到影响。
相关问题优化:松散节流
动作signal()
只能在每次局部变量a
(例如,系统时间样本)自上次signal()
增加了某个常数threshold >= 1
时调用一次被称为。这里,b
是与a
相比的阈值占位符,如果a >= b
则设置为a + threshold
。只有“获胜”的线程应该调用signal()
。
var initial = b;
if ((a > initial) && Interlocked.CompareExchange(ref b, a + threshold, initial) == initial)
signal();
这里,b
又是单调的,但现在我们可以完全优化重试循环,因为我们只想知道本地线程是否在尊重threshold
的同时与其他线程“赢得比赛”。此方法将有效地“阻止”任何其他线程在定义的阈值内发出信号。
限制注意事项
如果你需要一个“严格”的节流阀——即“最小的”a
a >= b
——这就是标题中的“松散”,这将不起作用。如果您只需要报告一系列紧密分组的a
s 中的最后一个(可能称为"debouncing",相关但不同的问题),这也不起作用。
【讨论】:
以上是关于比较结果的原子交换值的主要内容,如果未能解决你的问题,请参考以下文章