【问题标题】:Fast and Lock Free Single Writer, Multiple Reader快速且无锁的单写入器,多读取器
【发布时间】:2019-01-10 09:51:15
【问题描述】:

我有一个写入器,它必须以相当高的频率递增一个变量,还有一个或多个读取器以较低的频率访问这个变量。

写入由外部中断触发。

由于我需要高速写入,我不想使用互斥锁或其他昂贵的锁定机制。

我想出的方法是在写入后复制值。读者现在可以将原件与副本进行比较。如果相等,则变量的内容有效。

这是我在 C++ 中的实现

template<typename T>
class SafeValue
{
private:
    volatile T _value;
    volatile T _valueCheck;
public:
    void setValue(T newValue)
    {
        _value = newValue;
        _valueCheck = _value;
    }

    T getValue()
    {
        volatile T value;
        volatile T valueCheck;
        do
        {
            valueCheck = _valueCheck;
            value = _value;
        } while(value != valueCheck);

        return value;
    }
}

这背后的想法是在读取时检测数据竞争,并在发生时重试。但是,我不知道这是否总是有效。我在网上没有找到任何关于这种方法的信息,因此我的问题是:

我的方法与单个作者和多个阅读器一起使用时有什么问题吗?

我已经知道高写入频率可能会导致读者饿死。还有更多的不良影响我必须小心吗?难道这根本就不是线程安全的吗?

编辑 1:

我的目标系统是 ARM Cortex-A15。

T 至少应该能够成为任何原始整数类型。

编辑 2:

std::atomic 在读写器站点上太慢了。我在我的系统上对其进行了基准测试。与未受保护的原始操作相比,写入大约慢 30 倍,读取大约慢 50 倍。

【问题讨论】:

  • 评论不用于扩展讨论;这个对话是moved to chat
  • 您真的需要高速写入吗?难道你不能增加一个写入器私有变量,然后定期将它添加到读者可访问的总数中,而不是必须经常将总数增加1吗?换句话说,读者能容忍一点陈旧吗?
  • @Branko Dimitrijevic - 不多。计数器用于跟踪步进电机的位置。读者需要尽可能准确地获取该电机的位置。
  • @Detonar 我建议编辑问题以详细解释为什么原子不适合您。正如您在下面的评论中所写:std::atomic 的问题是内部系统调用大大减慢了它的速度。什么内部系统调用?仅当给定数据类型的硬件支持原子操作时,原子操作才应归结为特殊指令(您的情况是否如此?)。 放慢速度是什么意思?生产者放缓?消费者放缓?你如何衡量这种放缓?
  • @Daniel Langr - 我在目标系统上对其进行了基准测试。编辑了我的问题。

标签: c++ multithreading lock-free


【解决方案1】:

这个单一变量只是一个整数、指针还是普通的旧值类型,你可能只使用std::atomic

【讨论】:

  • std::atomic 太慢了,即使这样也不能保证无锁。
  • @Detonar 你的说法毫无意义。研究std::atomic生成的程序集。
  • @MaximEgorushkin 如果您阅读了这个问题,那不是真的。他只是想避免 CAS 循环。他试图解决的问题可以比std::atomic 更快地解决,因为他的任务更加具体。
  • @Ivan 只有一个写者,共享变量是int,不需要CAS循环。
  • @MaximEgorushkin 在 32 位平台上读取 8 字节值是使用 CAS 循环实现的,例如例如,在只读页面上会失败。我认为他想加快阅读器的速度。问题是他的代码根本不是“无锁”的。
【解决方案2】:

您应该首先尝试使用std::atomic,但要确保您的编译器知道并理解您的目标架构。由于您的目标是 Cortex-A15 (ARMv7-A cpu),因此请确保使用 -march=armv7-a 甚至 -mcpu=cortex-a15

第一个应生成 ldrexd 指令,根据 ARM 文档应该是原子的:

单拷贝原子性

在 ARMv7 中,单拷贝原子处理器访问是:

  • 所有字节访问
  • 对半字对齐位置的所有半字访问
  • 对字对齐位置的所有字访问
  • LDREXDSTREXD 指令对双字对齐位置的内存访问。

后者应生成ldrd 指令,该指令在支持大物理地址扩展的目标上应该是原子的:

在包含大型物理地址扩展的实现中,LDRDSTRD 对 64 位对齐位置的访问是 64 位单副本原子,如转换表遍历和对转换表的访问所示。

--- 注意---

Large Physical Address Extension 添加了此要求,以避免在更改转换表条目时需要采取复杂措施来避免原子性问题,而无需创建内存系统中的所有位置都是 64 位单副本原子的要求。

你也可以查看Linux内核implements那些:

#ifdef CONFIG_ARM_LPAE
static inline long long atomic64_read(const atomic64_t *v)
{
    long long result;

    __asm__ __volatile__("@ atomic64_read\n"
"   ldrd    %0, %H0, [%1]"
    : "=&r" (result)
    : "r" (&v->counter), "Qo" (v->counter)
    );

    return result;
}
#else
static inline long long atomic64_read(const atomic64_t *v)
{
    long long result;

    __asm__ __volatile__("@ atomic64_read\n"
"   ldrexd  %0, %H0, [%1]"
    : "=&r" (result)
    : "r" (&v->counter), "Qo" (v->counter)
    );

    return result;
}
#endif

【讨论】:

    【解决方案3】:

    没有人可以知道。您将必须查看您的编译器是否记录了任何多线程语义来保证这将起作用,或者查看生成的汇编代码并说服自己它会起作用。请注意,在后一种情况下,更高版本的编译器、不同的优化选项或更新的 CPU 总是可能会破坏代码。

    我建议使用适当的memory_order 测试std::atomic。如果由于某种原因太慢,请使用内联汇编。

    【讨论】:

    • 我希望专注于算法而不是语言特定的细节。以及如何使用内联汇编提供可行的性能提升? std::atomic 的问题是内部系统调用大大减慢了它的速度。
    • @Detonar 算法在这里非常简单,完全取决于实现的细节。使用内联汇编可以提供可行的性能提升,因为它可以让你 100% 确定你得到了你想要的东西,所以你不需要防御性地编码。防御性编码可能会让您付出代价。
    【解决方案4】:

    另一种选择是拥有一个发布者生成的非原子值缓冲区和一个指向最新值的原子指针。

    #include <atomic>
    #include <utility>
    
    template<class T>
    class PublisherValue {
        static auto constexpr N = 32;
        T values_[N];
        std::atomic<T*> current_{values_};
    
    public:
        PublisherValue() = default;
        PublisherValue(PublisherValue const&) = delete;
        PublisherValue& operator=(PublisherValue const&) = delete;
    
        // Single writer thread only.
        template<class U>
        void store(U&& value) {
            T* p = current_.load(std::memory_order_relaxed);
            if(++p == values_ + N)
                p = values_;
            *p = std::forward<U>(value);
            current_.store(p, std::memory_order_release); // (1) 
        }
    
        // Multiple readers. Make a copy to avoid referring the value for too long.
        T load() const {
            return *current_.load(std::memory_order_consume); // Sync with (1).
        }
    };
    

    This is wait-free,但在复制值时可能会取消调度读取器并因此读取已部分覆盖的最旧值的可能性很小。使N 更大可以降低这种风险。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2015-02-03
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2015-02-14
      相关资源
      最近更新 更多