c++与python共享内存实现
共享内存实际上就是进程通过调用shmget(Shared Memory GET 获取共享内存)来分配一个共享内存块,然后每个进程通过shmat(Shared Memory Attach 绑定到共享内存块),将进程的逻辑虚拟地址空间指向共享内存块中。 随后需要访问这个共享内存块的进程都必须将这个共享内存绑定到自己的地址空间中去。当一个进程往一个共享内存快中写入了数据,共享这个内存区域的所有进程就可用都看到其中的内容。
因为所有进程共享同一块内存,共享内存在各种进程间通信方式中具有最高的效率。访问共享内存区域和访问进程独有的内存区域一样快,并不需要通过系统调用或者其它需要切入内核的过程来完成。同时它也避免了对数据的各种不必要的复制。
- 共享内存是进程间共享数据的一种最快的方法。一个进程向共享的内存区域写入了数据,共享这个内存区域的所有进程就可以立刻看到其中的内容。
- 使用共享内存要注意的是多个进程之间对一个给定存储区访问的互斥。若一个进程正在向共享内存区写数据,则在它做完这一步操作前,别的进程不应当去读、写这些数据。
系统内核没有对访问共享内存进行同步,C++也没有提供相应的互斥机制,必须手动实现同步。
- c++编译动态库完成各种共享内存的实际操作。
- python/c++端调用动态库进行共享内存数据交互。
兼容 c++与python、c++与c++、python与python 任意 一对一、一对多、多对多 之间的跨进程大容量数据传输(多方同时数据交互需要自定义好传输规则、避免内存竞争数据损坏)。
# CMakeList.txt
# 编译生成 libsharememory.so 动态库
add_library(sharememory SHARED src/share_memory.h src/share_memory.cpp)
# 将共享内存加入项目
add_executable(cpp_python
test.cpp
src/share_memory.cpp src/share_memory.h)
使用方法:
// 引入头文件
#include "share_memory.h"
// 创建共享内存
ShareMem::ShareMemory ShareImpl(12331);
// 共享内存设为可写状态
ShareImpl.SetStatus(CAN_WRITE);
// 写入数据,数据类型为 u_char * ,必须要指定传输数据的大小
ShareImpl.PutShareBody((u_char *)image, image_size);
共享内存功能已经封装为可调用的python模块,模块目录结构如下:
─┬ PyShareMemory
│
├── __init__.py
│
├── ShareMemory.py
│
└── libsharememory.so
模块调用示例:
# 将模块放在项目根目录,导入共享内存模块
from PyShareMemory.ShareMemory import ShareMemory
# 创建共享内存,参数为共享内存的识别key
share = ShareMemory(12331)
# 读取共享内存的数据,格式为 ctypes.c_uint8
c_data = share.get_data()
# 转换为numpy数组(如果有需要)
np_data = np.array(c_data, dtype=np.uint8)
- OS: CentOS Linux 7 (Core) x86_64
- Host: Intel Corporation 440BX Desktop
- Kernel: 3.10.0-1160.el7.x86_64
- CPU: Intel Xeon Gold 6226R (20) @ 2.893G
- Memory: 64245MiB
- g++ (GCC) 8.3.1 20190311 (Red Hat 8.3.1-3)
- Python 3.8.8
-
统计结果均为平均耗时,即发送多张图片用总耗时除以发送次数(100次),由于最初建立通信的前两次传输不稳定,因此计算时时舍弃了前两帧;
-
发送的数据:由OpenCV生成的三通道彩色标准BGR图片;
-
返回的数据:接收完成的标志位(一个整形变量);
尺寸 | 大小(MB) | 发送耗时*(ms) | 收发耗时*(ms) | 传输效率(大小/收发耗时) | gRPC收发(ms) |
---|---|---|---|---|---|
640*640 | 1.17 | 0.20 | 0.21 | 5.57 | |
640*640 (4张) | 4.68 | 0.66 | 0.67 | 6.99 | |
640*640 (8张) | 9.37 | 1.33 | 1.34 | 6.99 | |
640*640 (16张) | 18.75 | 3.01 | 3.03 | 6.19 | |
720*360 | 0.74 | 0.11 | 0.12 | 6.17 | |
1920*1080 | 5.93 | 0.88 | 0.89 | 6.66 | |
1920*1080(4张) | 23.73 | 4.07 | 4.09 | 5.80 | |
1920*1080(8张) | 47.46 | 8.55 | 8.56 | 5.54 | |
极限大小 | 2002.71 | 367.74 | 367.79 | 5.45 |
发送耗时:相当于数据c++写入共享内存的耗时,统计的时间为c++端的写入耗时;
收发耗时:接收数据时会对上一次发送作校验,只有在上一次发送的数据被python端接收处理并返回处理成功的信息后才会发送下一帧。统计的时间为第n帧开始发送的时间至第n+1帧发送前的时间;
共享内存仅用于发送大体积数据(如大量图片),图片的发送状态、图片的处理结果(Python端应答)通过gRPC进行传输。可以解决死循环占用资源的问题。
优势:解决了死循环占用资源问题;
劣势:引入gRPC模块,增加了系统复杂性;gRPC通信延迟往往不低于1.2ms,系统增加了约2.4ms的通信时间
发送端将要发送的数据进行一定的拆分,使用多线程同时写入共享内存中。写入时根据每部分数据的大小计算好写入内存位置的偏移量,以保证写入完成后数据的完整性与连贯性。
由于读取内存数据耗时几乎可以忽略不计,因此对共享内存中的数据整段读取。
理论上将数据拆分为n份可以提高写入速度n倍,使用此方案可以大幅提高数据的写入速度。
- 使用管道传递Python的应答消息:【尝试级】将Python程序作为C++的子进程启动,通过进程间关联创建管道,使用管道信号作为应答载体。较复杂。可以解决死循环占用资源的问题。
- c++和python端分别再次封装成队列:【低优先级】将动态库进行二次封装,使用时单步调用发送/接受。