我正在使用内存映射文件进行跨进程数据共享。
我有两个进程,一个用于写入数据块,另一个用于读取这些块。为了让读者知道一个块是否准备就绪,我正在编写两个“标记”值,一个在开始处,一个在每个块的末尾,表示它已准备就绪。
它看起来像这样:
注意:在此示例中,我不包括读者进程可以寻找先前块的事实。
static const int32_t START_TAG = 0xFAFAFAFA;
static const int32_t END_TAG = 0x06060606;
void writer_process(int32_t* memory_mapped_file_ptr)
{
auto ptr = memory_mapped_file_ptr;
while (true)
{
std::vector<int32_t> chunk = generate_chunk();
std::copy(ptr + 2, chunk.begin(), chunk.end());
// We are done writing. Write the tags.
*ptr = START_TAG;
ptr += 1;
*ptr = chunk.size();
ptr += 1 + chunk.size();
*ptr = END_TAG;
ptr += 1;
}
}
void reader_process(int32_t* memory_mapped_file_ptr)
{
auto ptr = memory_mapped_file_ptr;
while (true)
{
auto ptr2 = ptr;
std::this_thread::sleep_for(std::chrono::milliseconds(20));
if (*ptr2 != START_TAG)
continue;
ptr2 += 1;
auto len = *ptr2;
ptr2 += 1;
if (*(ptr2 + len) != END_TAG)
continue;
std::vector<int32_t> chunk(ptr2, ptr2 + len);
process_chunk(chunk);
}
}
到目前为止这种作品。但它在我看来是一个非常糟糕的主意,并且可能由于缓存行为而导致各种奇怪的错误。
有没有更好的方法来实现这一目标?
我看过:
消息队列:效率低,仅适用于单个阅读器。此外,我无法寻求以前的块。
互斥锁:不确定如何仅锁定当前块而不是整个内存。我不能为每个可能的块提供互斥锁(特别是因为它们具有动态大小)。我已经考虑过将内存划分为每个块有一个互斥锁的块,但由于它在写入和读取之间的延迟,这对我不起作用。
答案 0 :(得分:1)
正如其他人所说,你需要有某种内存障碍,以确保在多个处理器(和进程)之间正确同步。
我建议您使用定义一组当前可用条目的标题更改您的方案,并在新条目可用时使用互锁增量。
http://msdn.microsoft.com/en-us/library/windows/desktop/ms683614%28v=vs.85%29.aspx
我建议的结构是这样的,所以你可以真正实现你想要的,并迅速做到:
// at the very start, the number of buffers you might have total
uint32_t m_size; // if you know the max. number maybe use a const instead...
// then m_size structures, one per buffer:
uint32_t m_offset0; // offset to your data
uint32_t m_size0; // size of that buffer
uint32_t m_busy0; // whether someone is working on the buffer
uint32_t m_offset1;
uint32_t m_size1;
uint32_t m_busy1;
...
uint32_t m_offsetN;
uint32_t m_sizeN;
uint32_t m_busyN;
使用偏移量和大小,您可以直接访问映射区域中的任何缓冲区。要分配缓冲区,您可能希望实现类似于malloc()的功能,尽管此表中的所有必要信息都可以在此处找到,因此不需要链接列表等。但是,如果要释放一些缓冲区,你需要跟踪它的大小。如果你一直分配/免费,你将获得分散的乐趣。总之...
另一种方法是使用环形缓冲区(本质上是“管道”),所以你总是在最后一个缓冲区之后分配,如果没有足够的空间,那么在最开始时分配,根据需要关闭N个缓冲区。新的缓冲区大小要求......这可能更容易实现。但是,这意味着您可能需要知道在寻找缓冲区时从哪里开始(即具有当前被认为是“第一个”[最旧]缓冲区的索引,这将恰好是下一个被重用的缓冲区。)
但是既然你没有解释缓冲区如何变得“旧”和可重用(释放以便可以重用),我真的无法给你一个确切的实现。但是像下面这样的东西可能会为你做这件事。
在头结构中,如果m_offset为零,则当前未分配缓冲区,因此与该条目无关。如果m_busy为零,则没有进程正在访问该缓冲区。我还提供了一个m_free字段,可以是0或1.只要需要更多缓冲区来保存刚收到的数据,编写器就会将该参数设置为1。我不会那么深,因为我不知道你是如何释放你的缓冲区的。如果你从不释放缓冲区也不是必需的。
0)结构
// only if the size varies between runs, otherwise use a constant like:
// namespace { uint32_t const COUNT = 123; }
struct header_count_t
{
uint32_t m_size;
};
struct header_t
{
uint32_t m_offset;
uint32_t m_size;
uint32_t m_busy; // to use with Interlocked...() you may want to use LONG instead
};
// and from your "ptr" you'd do:
header_count_t *header_count = (header_count_t *) ptr;
header_count->m_size = ...; // your dynamic size (if dynamic it needs to be)
header_t *header = (header_t *) (header_count + 1);
// first buffer will be at: data = (char *) (header + header_count->m_size)
for(size_t n(0); n < header_count->m_size; ++n)
{
// do work (see below) on header[n]
...
}
1)访问数据的编写者必须首先锁定缓冲区,如果不可用,请再次尝试使用下一个;使用InterlockedIncrement()
完成锁定并使用InterlockedDecrement()
解锁:
InterlockedIncrement(&header[n]->m_busy);
if(header[n]->m_offset == nullptr)
{
// buffer not allocated yet, allocate now and copy data,
// but do not save the offset until "much" later
uint32_t offset = malloc_buffer();
memcpy(ptr + offset, source_data, size);
header[n]->m_size = size;
// extra memory barrier to make sure that the data copied
// in the buffer is all there before we save the offset
InterlockedIncrement(&header[n]->m_busy);
header[n]->m_offset = offset;
InterlockedDecrement(&header[n]->m_busy);
}
InterlockedDecrement(&header[n]->m_busy);
现在,如果你想释放一个缓冲区,这还不够。在这种情况下,需要另一个标志来防止其他进程重用旧缓冲区。这又取决于你的实现......(见下面的例子。)
2)访问数据的读者必须先使用InterlockedIncrement()
缓冲区锁定缓冲区,然后需要使用InterlockedDecrement()
释放缓冲区。请注意,即使m_offset为nullptr,锁也适用。
InterlockedIncrement(&header[n]->m_busy);
if(header[n]->m_offset)
{
// do something with the buffer
uint32_t size(header[n]->m_size);
char const *buffer_ptr = ptr + header[n]->m_offset;
...
}
InterlockedDecrement(header[n]->m_busy);
所以在这里我只测试是否设置了m_offset。
3)如果你想释放缓冲区,你还需要测试另一个标志(见下文),如果另一个标志为真(或假),则缓冲区即将被释放(一旦释放缓冲区)所有进程都释放了它)然后该标志可用于前面的代码片段(即m_offset为零,或者该标志为1且m_busy
计数器正好为1。)
作家的这样的事情:
LONG lock = InterlockedIncrement(&header[n]->m_busy);
if(header[n]->m_offset == nullptr
|| (lock == 1 && header[n]->m_free == 1))
{
// new buffer (nullptr) or reusing an old buffer
// reset the offset first
InterlockedIncrement(&header[n]->m_busy);
header[n]->m_offset = nullptr;
InterlockedDecrement(&header[n]->m_busy);
// then clear m_free
header[n]->m_free = 0;
InterlockedIncrement(&header[n]->m_busy); // WARNING: you need another Decrement against this one...
// code as before (malloc_buffer, memcpy, save size & offset...)
...
}
InterlockedDecrement(&header[n]->m_busy);
在读者中,测试的变化是:
if(header[n]->m_offset && header[n]->m_free == 0)
作为旁注:所有Interlocked ...()函数都是完全的内存障碍(围栏),所以你在这方面都很好。你必须使用其中许多来确保你得到正确的同步。
请注意,这是未经测试的代码...但是如果你想避免进程间信号量(这可能不会简化这么多),那就是要走的路。请注意,不需要20ms的sleep(),除非为了避免每个读取器有一个挂钩的CPU,显然。