C++多线程打包成so给JAVA后端(Ubuntu)<1>

提及这个问题,就会有人问:为什么要用C++写成so让JAVA端调用,而不是让JAVA端自己写逻辑呢?

这是一个非常好的问题!它触及了系统架构设计的核心考量。用C++编写so让Java通过JNI调用,而不是全部用Java实现,主要是为了在性能、复用、能力这三个关键维度上取得平衡。

现在我们公司有个业务需要这样处理,之前是C++端写的处理逻辑,现在要将这部分功能放到Java服务台端调用,做成网络化软件,具体的原因这里不说了,我来说一下如何将so给Java调用吧!

之前我也写过类似的文章,是使用AndroidStudio将C++代码编译成so文件,当时是需要给平板端使用,现在不一样了,这次编译的so是要给Java后端使用,并运行在服务器中。这还是稍微有一点点差别的。

开发工具:无论是windows版本还是Ubuntu

系统,都是使用jdk-11.0.17 + VSCode进行开发。

当在windows环境下是需要编译成*.dll,在Ubuntu系统中需要打包成so的形式。

无论是在windows环境下还是Ubuntu环境下都可以共用一份代码,只需要针对Ubuntu环境做小小的改动就可以。

我来说一下我的代码中线程实现的核心功能:

1:固定心跳包线程

每间隔一秒需要轮巡一次数据,当五秒未收到消息时,发送心跳包检索设备是否在线。

2:多线程检测硬件设备做防抖处理

根据当前CPU的内核个数开启多线程,开启这个多线程主要是因为连接的硬件设备,在对硬件模型进行操作时有些传感器会来回抖动,导致会频繁触发某个点的开关,使用多线程做防抖操作,当上传的消息在500ms内未进行变化时,确定该点被触发。

注意:这里只为大家讲解如何实现多线程,至于硬件传感器是否使用合理的问题,不在这里做讨论了。

心跳包线程

1:启动线程

该线程用于监测所有设备的心跳,当连接30台设备性能是绰绰有余的~

|---------|-------------|
| 指标 | 预估数值 |
| CPU占用 | < 5% |
| 内存占用 | ~30MB |
| 线程上下文切换 | 可忽略 |
| 网络带宽 | 取决于每次访问的数量值 |

这种负载对于现代操作系统来说绰绰有余,所以完全可以用一个线程来监测。

当第一次添加设备编号时,才需要启动线程

cpp 复制代码
//构造函数中初始化变量
m_bRunning = false; //默认,线程未启动
//函数调用
void NewMiddlewareOperation::AddNewModel(std::string sModelId, jstring jsModeId, JNIEnv* env, jobject obj)
{
	auto pOperation = FindValidOperationPtr(sModelId, env, obj);
	if (pOperation)
	{
		//当前模型已经存在,不需要添加了
		return;
	}
	//存储设备id的映射,在回调JNI数据时使用
	{
		jstring jsCmdglobal = (jstring)env->NewGlobalRef(jsModeId);
		// 释放局部引用
		env->DeleteLocalRef(jsModeId);
		gManager::instance().m_mapCacheJStringID.insert(std::make_pair(sModelId, jsCmdglobal));
	}
	// 使用智能指针创建对象,自动管理生命周期
	m_mapModelPtr[sModelId] = std::make_shared<ModelOperationData>(sModelId);

	// 启动心跳线程(仅启动一次)双重检查锁定
	if (!m_bRunning)
	{
		std::string sPrintLog = "<soLog>create heart thread";
		gManager::instance().SoCallBackPrintLog(sPrintLog, env, obj);
		m_bRunning = true; //启动线程
		m_thread = std::thread(&NewMiddlewareOperation::OnThreadHeartWorker, this);
		// 3. 验证线程是否成功创建
		if (m_thread.joinable())
		{
			m_bRunning = true;
		}
		else
		{
			std::string log = "<soLog>thread full: un joinable";
			gManager::instance().SoCallBackPrintLog(log, env, obj);
		}
	}
}

函数解析:

参数sModelId:设备id已经由jstring转化成std::string类型

参数jsModelId:此时是未经转化的设备id

参数env:JNI环境指针,由调用接口自动传入

参数obj:Java对象引用,也是自动传入

疑问一:为什么我会将模型id的jstring类型以及std::string同时传入到函数中?

答疑:在C++代码中,每个设备id对应了一个操作处理类(类名:ModelOperationData)。我的存储容器是:

cpp 复制代码
std::map<std::string, std::shared_ptr<ModelOperationData>> m_mapModelPtr; //模型处理类

在这里我才用了智能指针的方式,在Java端也可以正常使用,我已经测试过了~

此时你肯定会有疑问:为什么不直接吧jstring当做key值呢?还可以减少转换!切记,一定不要将jstring作为C++map的key值!

jstring是指针不是实际的字符串,生命周期由JVM管理,有可能导致悬空指针。

疑问二:为什么要存储全局的jstring数据?

答疑:在与Java后端进行回调时,当一拖多设备时,需要知道给回调的设备id,如果在回调时临时转换会导致性能开销问题。尤其是在高频唤醒中性能问题更明显!

而且临时转换时最容易丢失销毁代码:env->DeleteLocalRef(jstr);

为了提高回调效率,这里我将模型id进行了存储,即使是连接30台设备,与临时转换相比较而言这个消耗也是忽略不计的。

2:线程实现

cpp 复制代码
void NewMiddlewareOperation::OnThreadHeartWorker()
{
	JNIEnv* env = nullptr;
	jint attachResult = gManager::instance().m_jvm->AttachCurrentThread((void**)&env, NULL);
	if (attachResult != JNI_OK) {
		fprintf(stderr, "FATAL: AttachCurrentThread failed, error code: %d\n", attachResult);
		// 处理错误
		return;
	}
	try
	{
		while (m_bRunning)
		{
			for (auto& pair : m_mapModelPtr) // 使用范围for循环,避免迭代器失效
			{
				// 通过智能指针获取原始指针
				pair.second->processThreadDetection(env, gManager::instance().m_globalObj);
			}
			std::this_thread::sleep_for(std::chrono::milliseconds(1000));
		}
	}
	catch (const std::exception& e)
	{
		fprintf(stderr, "Heartbeat thread exception: %s\n", e.what());
	}
	
	// 分离线程
	gManager::instance().m_jvm->DetachCurrentThread();
}

代码解析:

第一步:将线程附加到JVM中。

将当前原生线程附加到Java虚拟机中,获取当前线程的JNIEnv指针,并检查附加是否成功

第二步:while主循环逻辑

使用m_bRunning标志控制线程运行,遍历m_mapModelPtr映射表中所有的模型,并每间隔一秒执行一次循环。

第三步:线程结束时从JVM分离

我的这段代码中还有一些欠缺不足之处:异常处理不完善,我只给出了日志输出,具体的大家自行填充;没有验证JNIEnv的有效性;以及在进行循环时未进行mutex互斥量处理。

使用小结

到这里,简单的心跳包线程就可以在so中被启动,并能够间隔一秒进行一次数据回调了。

其实在代码中还有一些变量我没有详细说明,比如:gManager::instance().m_jvm以及gManager::instance().m_globalObj等等单例变量都没有解释,我会在后续文章中讲解如何用Java代码成功调用这个心跳包线程!

我是糯诺诺米团,一名C++程序媛~

相关推荐
Knight_AL2 小时前
Java 内存溢出(OOM)排查实战指南:从复现到 MAT Dump 分析
java·开发语言
刘宇涵492 小时前
递归Java
java
MSTcheng.2 小时前
【C++】平衡树优化实战:如何手搓一棵查找更快的 AVL 树?
开发语言·数据结构·c++·avl
superman超哥2 小时前
Rust 泛型参数的使用:零成本抽象的类型级编程
开发语言·后端·rust·零成本抽象·rust泛型参数·类型级编程
Thomas_YXQ2 小时前
Unity3D IL2CPP如何调用Burst
开发语言·unity·编辑器·游戏引擎
superman超哥2 小时前
仓颉并发调试利器:数据竞争检测的原理与实战
开发语言·仓颉编程语言·仓颉
代码不停2 小时前
Spring Boot快速入手
java·spring boot·后端
秦苒&2 小时前
【C语言】字符函数和字符串函数:字符分类函数 、字符转换函数 、 strlen 、strcpy、 strcat、strcmp的使用和模拟实现
c语言·开发语言
小白学大数据2 小时前
Python 网络爬虫:Scrapy 解析汽车之家报价与评测
开发语言·爬虫·python·scrapy