前言
多线程一直是后端开发一个回避不了的话题。凡是大型项目,对高并发要求一般不低。除去利用docker,k8s等框架进行负载均衡,动态扩容之外,多线程也是增强程序/系统并行处理能力的有效手段。恰巧虹软人脸识别用户中也有很多小伙伴经常为多线程的编程烦恼,今天笔者结合自己的开发项目,记录一下自己使用虹软算法包期间的踩坑爬坑经历,希望能给.net使用者一些避坑提示。同时声明,本文基于虹软人脸识别SDK 3.0 Windows C++ / X64版本(SDK下载链接:https://ai.arcsoft.com.cn/ucenter/resource/build/index.html#/addFreesdk/1002?from=index)作为讲解以及代码展示。
“坑”在哪里?
避坑识坑。使用虹软算法时,新手在多线程情况下,最容易出现2个问题。一是内存冲突(写保护内存错误),二是运行一段时间后,程序崩溃,监控后发现内存溢出现象。造成这种情景的原因是大多是目前的.Neter缺少C++编程基础,再者对C#的内存回收机制没有完全理解清晰。这也是很多开发者反应,自己在代码中明明使用了GC,为什么还会出现内存溢出,甚至对虹软SDK的内存回收提出质疑。笔者并非虹软员工,但因为工作原因,使用了人脸SDK1.2, 2.0, 2.1, 2.2, 3.0(3.1,4.0是企业版,没法测试,笔者是个人认证)以及人证SDK1.0,2.0这几个版本,并在部署上线前做过一定的压力测试。综合这2年半的运维情况,可以负责的告诉大家,虹软SDK的内存管理是可靠的,出现内存溢出,几乎肯定是使用者自身编程的问题。
由于虹软人脸SDK没有C#版本(希望后期出一个,毕竟有Java版,我们很不舒服),.Neter一直是封装C++版本使用。由于C++是直接操作内存(C#在safe模式下是线程安全语言,不直接操作内存),因此虹软引擎在初始化后,就将所需要的内存空间在内存中申请好了。引擎在被调用时,数据会写入到相关内存,如果多个线程调用同一个引擎执行相同操作,例如提取特征值,就将发生一个内存地址被同时修改的错误,即试图修改写保护内存。
避坑方案1:引擎捆绑法(一个引擎捆绑到一个线程)
.NET 4.0在线程方面加入了很多东西,其中就包括ThreadLocal
static void Main()
{
var local = new ThreadLocal<IntPtr>();
//修改TLS的线程
Thread th = new Thread(() =>
{
local.Value = intptr; //虹软引擎指针
DoSomething(); //虹软人脸对比具体流程
})
th.Start();
th.Join();
}
避坑方案2:引擎池法
.Net 4.0不仅引入了ThreadLocal
由于ConcurrentQueue是线程安全队列,我们不妨将引擎指针IntPtr变量放在里面,避免多线程情景被同时取用,用不需要手动在加锁,岂不美哉!思路如下(源码:https://github.com/18628271760/MultipleFacesProcess)
定义接口
public interface IEnginePoor
{
public ConcurrentQueue<Intptr> FaceEnginePoor{get;set; }
public IntPtr GetEngine(ConcurrentQueue<Intptr> queue);
public void PutEngine(ConcurrentQueue<Intptr> queue,IntPtr item);
}
实际使用
public override async Task RecongnizationByFace(IAsyncStreamReader requestStream,IServerStreamWriter responseStream, ServerCallContext context)
{
var faceQueue=new Queue();
IntPtr featurePoint=IntPtr.Zero;
IntPtr engine=FaceProcess.GetEngine(FaceProcess.FaceEnginePoor);
FaceReply faceReply=new FaceReply();
while(await requestStream.MoveNext())
{
//识别业务
byte[] featureByte=requestStream.Current.FaceFeature.ToByteArray();
if(featureByte.Length!=1032) //注意,2.x和3.x版本的人脸特征长度是1032.
{
continue;
}
featurePoint=Arcsoft_Face_Action.PutFeatureByteIntoFeatureIntPtr(featureByte);
float maxScore=0f;
while(engine==IntPtr.Zero)
{
Task.Delay(10).Wait();
engine=FaceProcess.GetEngine(FaceProcess.IDEnginePoor);
}
foreach(var f in StaticDataForTestUse.dbFaceInfor)
{
float result=0;
int compareStatus=Arcsoft_Face_3_0.ASFFaceFeatureCompare(engine, featurePoint, f.Key,ref result,1);
if(compareStatus==0)
{
if(result>=maxScore)
{
maxScore=result;
}
if(result>=_faceMix&&result>=maxScore)
{
faceReply.PersonName=f.Value;
faceReply.ConfidenceLevel=result;
}
}
else
{
faceReply.PersonName=$"对比异常 error code={compareStatus}";
faceReply.ConfidenceLevel=result;
}
}
if(maxScore<_faceMix)
{
faceReply.PersonName=$"未找到匹配者";
faceReply.ConfidenceLevel=maxScore;
}
Marshal.FreeHGlobal(featurePoint);
await responseStream.WriteAsync(faceReply);
}
FaceProcess.PutEngine(FaceProcess.FaceEnginePoor,engine);
}
除了线程调用引起的内存冲突外,引擎数量过多引起的内存溢出也是多线程情况下的一大痛点。引擎过少处理效率不足,引擎多了,出现程序崩溃。引擎数量如何规划?
避坑方案3:合理规划线程数量(引擎并发数量)
虹软的文档中友善的提示了大家,引擎的数量不超过系统内核数量(当然,我认为这建议比较保守,毕竟不同代数,不同档次的CPU的性能差别很大)。 对于初始化引擎数量,笔者根据自己的工程实践,做了以下总结供大家参考:
CPU方面:引擎数量根据系统CPU消耗情况估算,多个引擎同时处理时,CPU占有率不超过90%。
内存方面:每个引擎的内存消耗按400M估算(以3代SDK为例,其他版本可自行测试估算),系统内存占用不超过80%(注意:需要规划预留图片处理是需要的内存!)。
引擎数量取 CPU,内存限制数量中的最小值。
容器化部署尽量避免一个程序一个引擎(浪费注册码,增加资源消耗),但建议部署2,3个多引擎容器组成集群,并对每个容器做内存资源CPU限制,平衡稳定性,限制动态扩展容器数量。
总结
以上几点避坑建议是笔者从工程实践中得到的一点思考,欢迎更多的小伙伴尝试使用虹软SDK,对多线程方案有更好的建议,欢迎留言。
了解更多人脸识别产品相关内容请到虹软视觉开放平台哦