前言

多线程一直是后端开发一个回避不了的话题。凡是大型项目,对高并发要求一般不低。除去利用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类型,他的出现更大的简化了TLS的操作。当使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本,起到了线程隔离的作用。下面的代码,就是利用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。ConcurrentQueue队列是一个高效的线程安全的队列,ConcurrentQueue数据结构的示意图:

在这里插入图片描述
由于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,对多线程方案有更好的建议,欢迎留言。

了解更多人脸识别产品相关内容请到虹软视觉开放平台

Logo

K8S/Kubernetes社区为您提供最前沿的新闻资讯和知识内容

更多推荐