C# 如何创建一个超大(8MB)联合体
在项目上遇到了一个问题,我维护的网络视频播放客户端老是因为C++解码库的异常导致崩溃,所以决定把解码过程直接隔离到子进程里。这样:
- 我需要频繁的把主进程接收的视频帧数据,转发到子进程里解码渲染;
- 我需要偶尔与解码进程通信,如录像回放中命令解码器调整播放速度,或者接收解码器的帧缓存警告以调整视频流的转发速度。
因此,我需要一种在主子进程间传递消息的结构,它应该:
- 能快速序列化/反序列化;
- 能快速判断消息类型;
- 能承载大数据量;
- 能方便的把数据共享给C++解码库,并能快速释放。
对于目标4,因为托管堆上的内存由GC管理,且C++不能直接访问,所以考虑直接在非托管内存中申请内存,并自己释放内存。
对于目标1和2,考虑用C# 精确控制内存布局的结构体 来实现,通过精确控制内存布局,可以实现和 C++联合体 相似的数据结构。
对于目标3,C#要创建超大联合体,需要用到指针,因此需要在项目属性-生成里勾选允许不安全代码选项。
/// <summary>
/// 表示一个8M大小的信令
/// </summary>
[StructLayout(LayoutKind.Explicit)]
internal unsafe struct M8Token
{
[FieldOffset(0)]
public int tokenType;
[FieldOffset(4)]
public int dataLength;
[FieldOffset(0)]
public fixed byte data[1 << 23];
}
上面的代码中,StructLayout用于定义结构体的内存布局方式;LayoutKind.Explicit表示精确布局,即由FieldOffset指定结构体里每个字段在内存中对齐位置,单位为字节。
该结构体内存的0-3字节组成了tokenType,4-7字节组成了dataLength;从第8字节起,就是其携带的数据。
有意思的是data字段,这是一个byte指针,指向一个8MB大小的byte数组。FieldOffset对齐到0字节表示data指针指向结构体内存块的0号字节,因此M8Token结构体的大小就是8MB。
序列化时,只需通过data指针读取M8Token对象所有字节值;反序列化时,只需通过data指针按序填充byte数组就行了。
private static M8Token _token;
private static unsafe void Main(string[] args)
{
int sz = Marshal.SizeOf(typeof(M8Token));
_token = new M8Token();
// M8Token tmpToken = new M8Token(); // Error:*!!
M8Token* token1 = (M8Token*)Marshal.AllocHGlobal(sz);
token1->tokenType = 55;
token1->dataLength = 10;
byte* pbData1 = token1->data;
for (int i = 0; i < 10; i++)
{
pbData1[i + 8] = (byte)i;
// 或者
//*(pbData1 + i + 8) = (byte)i;
}
M8Token* token2 = (M8Token*)Marshal.AllocHGlobal(sz);
int dataLen = token1->dataLength + 8;
byte* pbData2 = token2->data;
for (int i = 0; i < dataLen; i++)
{
*(pbData2 + i) = *(pbData1 + i);
}
Console.WriteLine($"token type:{token2->tokenType}");
Console.WriteLine($"data len:{token2->dataLength}");
IntPtr dataPtr = (IntPtr)(pbData2 + 8);
SendSliceToCpp(dataPtr, token2->dataLength);
Marshal.FreeHGlobal((IntPtr)token1);
Marshal.FreeHGlobal((IntPtr)token2);
Console.ReadLine();
}
private static void SendSliceToCpp(IntPtr dataPtr, int dataLen)
{
unsafe
{
byte* pbDataArr = (byte*)dataPtr;
Console.Write("data:");
for (int i = 0; i < dataLen; i++)
{
Console.Write("," + pbDataArr[i]);
}
}
// 调用C++函数
}
结果:
token type:55
data len:10
data:,0,1,2,3,4,5,6,7,8,9
需要注意的是,M8Token大小为1<<23字节,也就是8MB,远大于.NET默认堆栈大小,直接在方法体里new一个局部变量会导致*异常。但是可以给一个类的字段new一个M8Token对象,这时M8Token对象在托管堆上分配内存所以不会导致*。
通过Marshal.AllocHGlobal(int size)申请的非托管内存块并返回该内存块的指针。实际上这个指针没有类型,我们按照M8Token的结构填充数据,C++代码可以直接通过这个指针访问内存中的数据。
当用完后,通过Marshal.FreeHGlobal(Intptr)立即释放非托管内存,防止内存泄漏。
上一篇: Python如何读取ini配置文件
推荐阅读