我有一个类存储一些传入的实时数据的最新值(大约1.5亿个事件/秒)。
假设它看起来像这样:
class DataState
{
Event latest_event;
public:
//pushes event atomically
void push_event(const Event __restrict__* e);
//pulls event atomically
Event pull_event();
};
我需要能够以原子方式推送事件并使用严格的排序保证来拉动它们。现在,我知道我可以使用自旋锁,但鉴于大量事件率(超过1亿/秒)和高度并发性,我更喜欢使用无锁操作。
问题是Event
的大小为64字节。任何当前的X86 CPU都没有CMPXCHG64B
指令(截至8月&#39; 16)。因此,如果我使用std::atomic<Event>
我必须链接到使用互联网下的互斥量的libatomic
(太慢)。
所以我的解决方案是以原子方式交换指向值的指针。问题是动态内存分配成为这些事件率的瓶颈。所以...我定义了一个我称之为&#34; ring allocator&#34;:
的东西/// @brief Lockfree Static short-lived allocator used for a ringbuffer
/// Elements are guaranteed to persist only for "size" calls to get_next()
template<typename T> class RingAllocator {
T *arena;
std::atomic_size_t arena_idx;
const std::size_t arena_size;
public:
/// @brief Creates a new RingAllocator
/// @param size The number of elements in the underlying arena. Make this large enough to avoid overwriting fresh data
RingAllocator<T>(std::size_t size) : arena_size(size)
{
//allocate pool
arena = new T[size];
//zero out pool
std::memset(arena, 0, sizeof(T) * size);
arena_idx = 0;
}
~RingAllocator()
{
delete[] arena;
}
/// @brief Return next element's pointer. Thread-safe
/// @return pointer to next available element
T *get_next()
{
return &arena[arena_idx.exchange(arena_idx++ % arena_size)];
}
};
然后我可以让我的DataState类看起来像这样:
class DataState
{
std::atomic<Event*> latest_event;
RingAllocator<Event> event_allocator;
public:
//pushes event atomically
void push_event(const Event __restrict__* e)
{
//store event
Event *new_ptr = event_allocator.get_next()
*new_ptr = *e;
//swap event pointers
latest_event.store(new_ptr, std::memory_order_release);
}
//pulls event atomically
Event pull_event()
{
return *(latest_event.load(std::memory_order_acquire));
}
};
只要将我的ring分配器调整为可以同时调用函数的最大线程数,就不会有覆盖pull_event可能返回的数据的风险。除了一切超级本地化,所以间接不会导致错误的缓存性能。这种方法有任何可能的陷阱吗?
答案 0 :(得分:2)
DataState
类:我认为这将是一个堆栈或队列,但它不是,所以push
/ pull
似乎不是方法的好名字。 (否则实施完全是假的)。
它只是一个锁存器,可以让你读取任何线程存储的最后一个事件。
没有什么可以阻止连续两次写入覆盖一个从未被读过的元素。也没有什么可以阻止你两次阅读相同的元素。
如果你只是需要某个地方来复制小块数据,那么环形缓冲区似乎是一种不错的方法。但如果你不想丢失事件,我不认为你可以这样使用它。相反,只需获取一个环形缓冲区条目,然后复制到它并在那里使用它。所以唯一的原子操作应该是增加环形缓冲区位置索引。
您可以提高get_next()
效率。该行执行原子后增量(fetch_add)和原子交换:
return &arena[arena_idx.exchange(arena_idx++ % arena_size)];
我甚至不确定它是否安全,因为xchg可以从另一个线程踩到fetch_add。无论如何,即使它安全,它也不理想。
你不需要那个。确保arena_size始终是2的幂,然后您不需要模拟共享计数器。你可以放手,并让每个线程模块化以供自己使用。它最终会换行,但它是一个二进制整数,所以它会以2的幂为单位,这是你竞技场大小的倍数。
我建议存储AND掩码而不是大小,因此%
编译除and
指令以外的任何内容都没有风险,即使它& #39;不是编译时常量。这可以确保我们避免使用64位整数div
指令。
template<typename T> class RingAllocator {
T *arena;
std::atomic_size_t arena_idx;
const std::size_t size_mask; // maybe even make this a template parameter?
public:
RingAllocator<T>(std::size_t size)
: arena_idx(0), size_mask(size-1)
{
// verify that size is actually a power of two, so the mask is all-ones in the low bits, and all-zeros in the high bits.
// so that i % size == i & size_mask for all i
...
}
...
T *get_next() {
size_t idx = arena_idx.fetch_add(1, std::memory_order_relaxed); // still atomic, but we don't care which order different threads take blocks in
idx &= size_mask; // modulo our local copy of the idx
return &arena[idx];
}
};
如果您使用calloc
而不是新的+ memset,则分配竞技场会更有效率。操作系统在将页面提供给用户空间进程之前已经将页面归零(以防止信息泄漏),因此编写它们只是浪费了工作。
arena = new T[size];
std::memset(arena, 0, sizeof(T) * size);
// vs.
arena = (T*)calloc(size, sizeof(T));
自己编写页面会对它们造成错误,因此它们全部连接到真实的物理页面,而不仅仅是系统范围的共享物理零页面的写时复制映射(就像它们在新的/之后)的malloc /释放calloc)。在NUMA系统上,所选择的物理页面可能取决于实际触及页面的线程,而不是哪个线程进行了分配。但是,由于您重新使用该池,因此编写页面的第一个核心可能不是最终使用它的核心。
可能需要在microbenchmarks / perf计数器中查找。
答案 1 :(得分:1)
只要将我的ring分配器调整为可以同时调用函数的最大线程数,就不会有覆盖pull_event可能返回的数据的风险。 ....这种方法有任何可能的陷阱吗?
陷入困境的是,IIUC,你的陈述是错误的。
如果我只有2个线程,并且在环形缓冲区中有10个元素,则第一个线程可以调用pull_event一次,然后是#34; mid-pulling&#34;,然后第二个线程可以调用push 10次,覆盖线程1正在拉动的东西。
再次,假设我正确理解您的代码。
另外,如上所述,
return &arena[arena_idx.exchange(arena_idx++ % arena_size)];
同一个变量交换中的arena_idx++
只是看起来不对。事实上是错误的。两个线程可以递增它 - ThreadA递增到8并且threadB递增到9,然后threadB将它交换为9,然后threadA将它交换为8. whoops。
atomic(op1)@ atomic(op2)!= atomic(op1 @ op2)
我担心未显示的代码还有什么问题。我并不是说侮辱 - 无锁是不容易的。
答案 2 :(得分:0)
您是否查看了可用的任何C ++ Disruptor(Java)端口?
虽然它们不是完整的端口,但它们可能提供您所需要的一切。我目前正在开发一个功能更全面的端口,但它还没有完全准备好。