【问题标题】:How to implement "Interlocked Compare exchange if less"?如何实现“如果少则互锁比较交换”?
【发布时间】:2016-11-17 06:14:25
【问题描述】:

我有一段旧代码:

if (current_Value > g_max_Value) g_max_Value=current_Value

正如您所了解的所有现代超级多线程、多 CPU 和巨大的 CPU 缓存,这段代码不能很好地工作。 问题:如何写得既可靠又优雅?

快速解决方案是将其包装在关键部分中。但如果我理解正确,这并不能保证 CPU 级别的原子性。

【问题讨论】:

  • 使用编译器内置函数(如果适用)。我们无法告诉您更多信息,因为您甚至没有指定架构或编译器。
  • 从 C11 开始的现代 C 具有原子数据类型,您可以使用 atomic_compare_exchange_strong,例如
  • 最后一句是什么意思?当然,一个关键部分,一个互斥体,将是原子的。
  • @2501:用锁/关键部分保护它与使其原子化并不完全相同。您不能对可以在信号处理程序中访问的内容使用锁定。
  • @Jester:ISO C11 提供原子,所以我们不需要任何编译器或特定架构。这作为一个纯 C 问题很好。

标签: c multithreading assembly multiprocessing


【解决方案1】:

如果多个线程可能同时更新g_max_Value,您需要一个原子cmpxchg。

如果不是,那么你就不会,即使其他线程可能正在读取它,而一个线程正在写入它。您可能仍需要确保存储和加载是原子的,但如果只有一个线程同时写入,则不需要昂贵的原子读取-修改-写入。

如果您对更新对其他线程可见的顺序有任何要求,那么您还需要release / acquire memory ordering 或类似的东西。如果不是,那么“宽松”的内存排序将确保操作是原子的,但不会在内存屏障上浪费指令或在编译时停止优化器重新排序。


ISO C11 已经提供atomic compare-exchange 作为语言的一部分。当然,这是一个交换如果相等,因为这是硬件通常提供的,所以你需要一个循环来重试。

基本思想是对大于进行比较,然后使用原子 cmpxchg 进行交换,因此只有在全局未更改时才会进行交换(因此比较结果仍然有效)。 如果自比较后发生变化,请重试。

#include <stdatomic.h>
#include <stdbool.h>

atomic_int g_max_Value;

// if (current_Value > g_max_Value) g_max_Value=current_Value
bool update_gmaxval(int cur)
{
    int tmpg = atomic_load_explicit(&g_max_Value, memory_order_relaxed);
    if (cur <= tmpg)
        return false;

    // global value may change here but still be less than cur, so we need a loop insted of just a single cmpxchg_strong

    while (!atomic_compare_exchange_weak_explicit(
             &g_max_Value, &tmpg, cur,
             memory_order_relaxed, memory_order_relaxed))
    {
        if (cur <= tmpg)
            return false;
    }
    return true;
}

我们可以通过更改为do{}while() 循环来简化:

// if (current_Value > g_max_Value) g_max_Value=current_Value
bool update_gmaxval_v2(int cur)
{
    int tmpg = atomic_load_explicit(&g_max_Value, memory_order_relaxed);

    // global value may change here but still be less than cur, so we need a loop insted of just a single cmpxchg_strong

    do {
        if (cur <= tmpg)
            return false;
    } while (!atomic_compare_exchange_weak_explicit(
             &g_max_Value, &tmpg, cur,
             memory_order_relaxed, memory_order_relaxed));
    return true;
}

这会编译成不同的代码,但我不确定它是否更好。


如果我们不返回真/假,我们会得到更高效的代码:

我把代码放在Godbolt compiler explorer 上看看它是否编译并查看asm。不幸的是,Godbolt 的 ARM/ARM64/PPC 编译器太旧(gcc 4.8),并且不支持 C11 stdatomic,所以我只能查看 x86 asm,我使用 memory_order_relaxed 而不是 memory_order_seq_cst 并不重要(locked 指令已经是完整的内存屏障,正常加载是隐式获取加载)。

我确实注意到这些包装器可以编译成更紧凑的代码

void update_gmaxval_void(int cur) { update_gmaxval(cur); }
void update_gmaxval_v2_void(int cur) { update_gmaxval_v2(cur); }

因为它们不必返回值。

【讨论】:

  • gcc 4.8 不提供stdatomics(没有定义__STDC_NO_ATOMICS__,不少于),但它确实有效地支持与@987654334 相同的功能和API @内置函数。如果您将atomic_load_explicit() 替换为__atomic_load_n() 并将atomic_compare_exchange_weak_explicit() 替换为__atomic_compare_exchange_n(),您应该能够测试ARM/POWER 版本。
  • @EOF:有趣,谢谢。当我真正关心的时候,我会记住这一点:P 这次我只是有点好奇,我希望这只是通常的 LDREX/STREX 加载链接/存储条件。
  • @EOF:该死的,直到你的评论让我想得更多,我才好奇。我在本地安装了一个更新的 ARM gcc 交叉编译器。循环的关键部分是:将期望值放入r1,然后ldrex r3, [r2]/cmp r3, r1/strexeq lr, r0, [r2]/cmpeq lr, #0/bne .L41。所以它使用谓词strex。这很时髦:P
  • @Downvoter:我的答案的哪一部分是错误的或需要改进?这不是我有史以来最伟大的作品,而且有点漫无边际,但我相当有信心我发布的功能将正常工作。我没有为不需要原子 cmpxchg 的版本编写函数,因为可能性太多。
猜你喜欢
  • 2013-10-27
  • 1970-01-01
  • 2013-08-26
  • 1970-01-01
  • 1970-01-01
  • 2010-12-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多