比较结果的原子交换值

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 是一个局部变量或至少不受异步写入的影响。 ab 都可以在你背后修改,那么就没有无锁的方式来原子地进行更新。 (感谢 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 是本地的,因此ab 都可能被其他线程访问
object lockObject = new object();
int a = 10;
int b = 5;

lock (lockObject)

   if (a > b)
   
      b = a
   

【讨论】:

我对这个答案持中立态度,因为它是正确的,尽管自以为是,但它给出了一个不错的理由。但我讨厌看到它得到反对票,所以赞成0【参考方案4】:

这些是我在阅读其他答案后提出的一些优化“食谱”。与问题没有直接关系,但在此处添加,因为这是相关搜索的位置。试图将其保留为 a &gt; ba &gt;= b 以匹配原始问题。并使它们保持一般性,以免使描述偏离其他相关问题。两者都可以封装到方法中。

优化 - 单调b

如果这是对bb 执行的唯一操作,否则单调递增,您可以优化互锁分配和重试其中a &gt; b == false:

int initial;
do 

  initial = b;

while ((a > initial) && Interlocked.CompareExchange(ref b, a, initial) != initial);

如果a &gt; b == false 在任何重试后都不会是trueb 只会越来越大)并且可以跳过交换,因为b 不会受到影响。

相关问题优化:松散节流

动作signal() 只能在每次局部变量a(例如,系统时间样本)自上次signal() 增加了某个常数threshold &gt;= 1 时调用一次被称为。这里,b 是与a 相比的阈值占位符,如果a &gt;= b 则设置为a + threshold。只有“获胜”的线程应该调用signal()

var initial = b;
if ((a > initial) && Interlocked.CompareExchange(ref b, a + threshold, initial) == initial)

  signal();

这里,b 又是单调的,但现在我们可以完全优化重试循环,因为我们只想知道本地线程是否在尊重threshold 的同时与其他线程“赢得比赛”。此方法将有效地“阻止”任何其他线程在定义的阈值内发出信号。

限制注意事项

如果你需要一个“严格”的节流阀——即“最小的”aa &gt;= b——这就是标题中的“松散”,这将不起作用。如果您只需要报告一系列紧密分组的as 中的最后一个(可能称为"debouncing",相关但不同的问题),这也不起作用。

【讨论】:

以上是关于比较结果的原子交换值的主要内容,如果未能解决你的问题,请参考以下文章

比较和交换 C++0x

原子操作

以原子方式比较-交换 Django 中的模型字段

gcc 内置原子比较和交换

CAS

使用 C++ 原子库,我应该使用啥内存顺序进行加载,然后进行比较交换?