快速且无锁的单写器、多读器

Fast and Lock Free Single Writer, Multiple Reader

本文关键字:单写器      更新时间:2023-10-16

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

写入由外部中断触发。

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

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

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

CCD_ 1应该能够成为至少任何基元积分类型。

编辑2:

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

如果这个变量只是一个整数、指针或普通的旧值类型,您可能只需要使用std::atomic。

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

第一个应根据ARM文档生成ldrexd指令:

单拷贝原子性

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

  • 所有字节访问
  • 对半字对齐位置的所有半字访问
  • 对单词对齐位置的所有单词访问
  • 由CCD_ 7和CCD_

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

在包括大型物理地址扩展的实现中,对64位对齐位置的T0和STRD访问是64位单拷贝原子,如翻译表遍历和对翻译表的访问所示。

---注意---

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

您还可以检查Linux内核是如何实现这些功能的:

#ifdef CONFIG_ARM_LPAE
static inline long long atomic64_read(const atomic64_t *v)
{
long long result;
__asm__ __volatile__("@ atomic64_readn"
"   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_readn"
"   ldrexd  %0, %H0, [%1]"
: "=&r" (result)
: "r" (&v->counter), "Qo" (v->counter)
);
return result;
}
#endif

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

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

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

#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).
}
};

这是免费等待的,但读卡器在复制值时可能会被取消计划,从而在其被部分覆盖时读取最旧的值。增大N可以降低这种风险。