C# 如何创建一个超大(8MB)联合体

在项目上遇到了一个问题,我维护的网络视频播放客户端老是因为C++解码库的异常导致崩溃,所以决定把解码过程直接隔离到子进程里。这样:

  • 我需要频繁的把主进程接收的视频帧数据,转发到子进程里解码渲染;
  • 我需要偶尔与解码进程通信,如录像回放中命令解码器调整播放速度,或者接收解码器的帧缓存警告以调整视频流的转发速度。

因此,我需要一种在主子进程间传递消息的结构,它应该:

  1. 能快速序列化/反序列化;
  2. 能快速判断消息类型;
  3. 能承载大数据量;
  4. 能方便的把数据共享给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:StackOverflow!!
            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一个局部变量会导致StackOverflow异常。但是可以给一个类的字段new一个M8Token对象,这时M8Token对象在托管堆上分配内存所以不会导致StackOverflow。

    通过Marshal.AllocHGlobal(int size)申请的非托管内存块并返回该内存块的指针。实际上这个指针没有类型,我们按照M8Token的结构填充数据,C++代码可以直接通过这个指针访问内存中的数据。

    当用完后,通过Marshal.FreeHGlobal(Intptr)立即释放非托管内存,防止内存泄漏。

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值