快速,无锁定的单个编写器,多个读取器

时间:2019-01-10 09:51:16

标签: c++ multithreading lock-free

我只有一个编写器,它必须以相当高的频率递增变量,并且还有一个或多个以较低频率访问此变量的读者。

写入是由外部中断触发的。

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

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

这是我在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倍。

4 个答案:

答案 0 :(得分:3)

此单个变量是整数,指针还是普通旧值类型,您可能只需要使用std::atomic

答案 1 :(得分:2)

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

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

  

单拷贝原子性

     

在ARMv7中,单副本原子处理器访问为:

     
      
  • 所有字节访问
  •   
  • 所有半字访问半字对齐的位置
  •   
  • 所有单词都可以访问单词对齐的位置
  •   
  • LDREXDSTREXD指令引起的对双字对齐位置的内存访问。
  •   

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

  

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

     

---注意---

     

大型物理地址扩展添加了此要求,以避免在更改转换表条目时避免采取复杂措施以避免原子性问题,而又不要求内存系统中的所有位置都是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

答案 2 :(得分:1)

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

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

答案 3 :(得分:0)

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

#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可以降低这种风险。