我正在创建驻留在内存中的仅追加数据结构,并将序列化为字节数组的记录追加到该内存。我需要它是线程安全的并且非常快,所以我想出了下面的代码,到目前为止效果很好(这是一个伪代码,实际版本更复杂,并且做了其他一些事情,但这只是为了理解这个想法)>
public sealed class MemoryList : IDisposable
{
private int nextOffset = 0;
private readonly MemoryMappedFile file;
private readonly MemoryMappedViewAccessor va;
public MemoryList(uint capacity)
{
// Some checks on capacity here
var mapName = Guid.NewGuid().ToString("N");
this.file = MemoryMappedFile.CreateNew(mapName, capacity);
this.va = file.CreateViewAccessor(0, capacity);
}
public void AppendMessage(byte[] messagePayload)
{
if (messagePayload == null)
throw new ArgumentNullException(nameof(messagePayload));
if (messagePayload.Length == 0)
throw new ArgumentOutOfRangeException(nameof(messagePayload));
if (TryReserveCapacity(messagePayload.Length, out var offsetToWriteTo))
{
this.va.Write(offsetToWriteTo, messagePayload.Length);
this.va.WriteArray(offsetToWriteTo + sizeof(int), messagePayload, 0, messagePayload.Length);
}
}
private bool TryReserveCapacity(int dataLength, out long reservedOffset)
{
// reserve enough room to store data + its size
var packetSize = sizeof(int) + dataLength;
reservedOffset = Interlocked.Add(ref this.nextOffset, packetSize) - packetSize;
if (this.nextOffset <= this.va.Capacity)
return true;
reservedOffset = -1;
return false;
}
public void Dispose()
{
file?.Dispose();
va?.Dispose();
}
}
这非常快,而且效果很好。无论我多么努力,我都无法打破它。
因此,现在我需要为每个附加消息使用TryReserveCapacity
方法来输出每个消息的逻辑索引。
因此,对于第一条消息,获取索引0,对于第二条消息-获取索引1,依此类推。
这导致对Interlocked
使用两个调用offset
,对messageIndex
使用两个调用,这显然不是线程安全的,我最终可能遇到导致以下情况的竞争条件。 / p>
MI:101,偏移:10000 MI:100,偏移:10500
关于如何保证没有一个MI会比另一个具有较大偏移量的MI大的想法?所有这些都没有使用任何锁?
那么,基本上,我们如何更改以下方法以使其行为正确?
private bool TryReserveCapacity(int dataLength, out long reservedOffset, out long messageId)
{
// reserve enough room to store data + its size
var packetSize = sizeof(int) + dataLength;
reservedOffset = Interlocked.Add(ref this.nextOffset, packetSize) - packetSize;
messageId = Interlocked.Increment(ref this.currentMessageId);
if (this.nextOffset <= this.va.Capacity)
return true;
reservedOffset = -1;
return false;
}
P.S我知道示例代码的字节序问题,但正如我所说,只是将其视为说明问题的伪代码。
答案 0 :(得分:1)
很抱歉,如果这不能直接解决您的主要问题(非锁定原子性),但是我看到您正在使用MemoryMappedFile
和MemoryMappedViewAccessor
类来操纵内存映射文件。
我真的不知道.NET Framework的当前迭代是否已解决此问题,但是在大约三年前编写的代码库中,我们发现使用这些类提供的内存映射文件操作确实很差的性能(如果我没记错的话,性能要慢7倍左右),即使是在托管 C ++ / CLI类内部,与使用Win32 API和对映射内存的直接指针操作相比,
我强烈建议您对这种方法进行测试,您可能会对性能提升感到惊讶(就像我们当然所做的那样),并且性能提升如此显着,以至于您可以负担标准锁定的成本来实现您想要的原子性。
如果您想探索这一途径,请参考以下代码片段,了解该技术的基础。
Int32 StationHashStorage::Open() {
msclr::lock lock(_syncRoot);
if( _isOpen )
return 0;
String^ fileName = GetFullFileName();
_szInBytes = ComputeFileSizeInBytes(fileName);
String^ mapExtension = GetFileExtension();
String^ mapName = String::Format("{0}{1}_{2}", _stationId, _date.ToString("yyyyMMdd"), mapExtension);
marshal_context context;
LPCTSTR pMapName = context.marshal_as<const TCHAR*>(mapName);
{
msclr::lock lock( _openLock );
// Try to see if another storage instance has requested the same memory-mapped file and share it
_hMapping = OpenFileMapping(FILE_MAP_READ | FILE_MAP_WRITE, FALSE, pMapName);
if( !_hMapping ) {
// This is the first instance acquiring the file
LPCTSTR pFileName = context.marshal_as<const TCHAR*>(fileName);
// Try to open the existing file, or create new one if not exists
_hFile = CreateFile(pFileName,
GENERIC_READ | GENERIC_WRITE,
FILE_SHARE_READ,
NULL,
OPEN_ALWAYS,
FILE_ATTRIBUTE_NORMAL,
NULL);
if( !_hFile )
throw gcnew IOException(String::Format(Strings::CreateFileFailed, GetLastError(), _stationId));
_hMapping = CreateFileMapping(_hFile,
NULL,
PAGE_READWRITE | SEC_COMMIT,
0,
_szInBytes,
pMapName);
if( !_hMapping )
throw gcnew IOException(String::Format(Strings::CreateMappingFailed, GetLastError(), _stationId));
_usingSharedFile = false;
} else {
_usingSharedFile = true;
}
}
// _pData gives you access to the entire requested memory range, you can directly
// dereference it, memcopy it, etc.
_pData = (UInt32*)::MapViewOfFile(_hMapping, FILE_MAP_READ | FILE_MAP_WRITE, 0, 0, 0);
if( !_pData )
throw gcnew IOException(String::Format(Strings::MapViewOfFileFailed, ::GetLastError(), _stationId));
// warm-up the view by touching every page
Int32 dummy = 0;
for( int i = 0; i < _szInBytes / sizeof(Int32); i+= 1024 ) {
dummy ^= _pData[i];
}
// return the dummy value to prevent the optimizer from removing the apparently useless loop
_isOpen = true;
return dummy;
}
void StationHashStorage::Cleanup() {
if( !_disposed ) {
// dispose unmanaged resources here
if( _pData ) {
if( !UnmapViewOfFile(_pData) )
LOG_ERROR(Strings::UnmapViewOfFileFailed, ::GetLastError(), _stationId);
_pData = NULL;
}
if( _hMapping ) {
if( !CloseHandle(_hMapping) )
LOG_ERROR(Strings::CloseMappingFailed, ::GetLastError(), _stationId);
_hMapping = NULL;
}
if( _hFile ) {
if( !CloseHandle(_hFile) )
LOG_ERROR(Strings::CloseFileFailed, ::GetLastError(), _stationId);
_hFile = NULL;
}
_disposed = true;
}
}
现在,关于您的 real 问题。您是否有可能将生成的ID嵌入到数据流中? 我的想法是这样的:
用一个虚拟的已知值(也许是0xffffffff)预写存储器的全部内容。
使用当前的容量检查原子逻辑。
写入消息有效负载后,您立即写入计算出的消息ID(您的容量检查将需要考虑此额外数据)
而不是使用Interlocked.Add来获取下一个ID,您将进入一个循环,该循环检查当前消息(先前消息ID)之前的 ,直到与您的消息不同为止。虚拟已知值。退出循环后,当前消息ID将为读取值+ 1。
这将需要对第一个插入的消息进行一些特殊的操作(因为它需要在流中播种第一个Id标记。您还需要小心(如果您使用的是长Id并且处于32位模式) ),您的ID流读写是原子的。
祝您好运,我真的鼓励您尝试一下Win32 API,找出希望有所改善的事情将非常有趣!如果您需要有关C ++ / CLI代码的帮助,请随时与我联系。