虹軟開發(fā)心得---多線程實(shí)戰(zhàn)開發(fā)避坑分享(C#)

前言

多線程一直是后端開發(fā)一個(gè)回避不了的話題。凡是大型項(xiàng)目,對(duì)高并發(fā)要求一般不低。除去利用docker,k8s等框架進(jìn)行負(fù)載均衡,動(dòng)態(tài)擴(kuò)容之外,多線程也是增強(qiáng)程序/系統(tǒng)并行處理能力的有效手段。恰巧虹軟人臉識(shí)別用戶中也有很多小伙伴經(jīng)常為多線程的編程煩惱,今天筆者結(jié)合自己的開發(fā)項(xiàng)目,記錄一下自己使用虹軟算法包期間的踩坑爬坑經(jīng)歷,希望能給.net使用者一些避坑提示。同時(shí)聲明,本文基于虹軟人臉識(shí)別SDK 3.0 Windows C++ / X64版本(SDK下載鏈接:https://ai.arcsoft.com.cn/ucenter/resource/build/index.html#/addFreesdk/1002?from=index)作為講解以及代碼展示。

“坑”在哪里?

避坑識(shí)坑。使用虹軟算法時(shí),新手在多線程情況下,最容易出現(xiàn)2個(gè)問題。一是內(nèi)存沖突(寫保護(hù)內(nèi)存錯(cuò)誤),二是運(yùn)行一段時(shí)間后,程序崩潰,監(jiān)控后發(fā)現(xiàn)內(nèi)存溢出現(xiàn)象。造成這種情景的原因是大多是目前的.Neter缺少C++編程基礎(chǔ),再者對(duì)C#的內(nèi)存回收機(jī)制沒有完全理解清晰。這也是很多開發(fā)者反應(yīng),自己在代碼中明明使用了GC,為什么還會(huì)出現(xiàn)內(nèi)存溢出,甚至對(duì)虹軟SDK的內(nèi)存回收提出質(zhì)疑。筆者并非虹軟員工,但因?yàn)楣ぷ髟?,使用了人臉SDK1.2, 2.0, 2.1, 2.2, 3.0(3.1,4.0是企業(yè)版,沒法測(cè)試,筆者是個(gè)人認(rèn)證)以及人證SDK1.0,2.0這幾個(gè)版本,并在部署上線前做過一定的壓力測(cè)試。綜合這2年半的運(yùn)維情況,可以負(fù)責(zé)的告訴大家,虹軟SDK的內(nèi)存管理是可靠的,出現(xiàn)內(nèi)存溢出,幾乎肯定是使用者自身編程的問題。
由于虹軟人臉SDK沒有C#版本(希望后期出一個(gè),畢竟有Java版,我們很不舒服),.Neter一直是封裝C++版本使用。由于C++是直接操作內(nèi)存(C#在safe模式下是線程安全語(yǔ)言,不直接操作內(nèi)存),因此虹軟引擎在初始化后,就將所需要的內(nèi)存空間在內(nèi)存中申請(qǐng)好了。引擎在被調(diào)用時(shí),數(shù)據(jù)會(huì)寫入到相關(guān)內(nèi)存,如果多個(gè)線程調(diào)用同一個(gè)引擎執(zhí)行相同操作,例如提取特征值,就將發(fā)生一個(gè)內(nèi)存地址被同時(shí)修改的錯(cuò)誤,即試圖修改寫保護(hù)內(nèi)存。

避坑方案1:引擎捆綁法(一個(gè)引擎捆綁到一個(gè)線程)

.NET 4.0在線程方面加入了很多東西,其中就包括ThreadLocal<T>類型,他的出現(xiàn)更大的簡(jiǎn)化了TLS的操作。當(dāng)使用ThreadLocal維護(hù)變量時(shí),ThreadLocal為每個(gè)使用該變量的線程提供獨(dú)立的變量副本,所以每一個(gè)線程都可以獨(dú)立地改變自己的副本,而不會(huì)影響其它線程所對(duì)應(yīng)的副本,起到了線程隔離的作用。下面的代碼,就是利用ThreadLocal確保多線程下,不同引擎安全運(yùn)行。

static void Main()
{
  var local = new ThreadLocal<IntPtr>();
  //修改TLS的線程
  Thread th = new Thread(() =>
  {
    local.Value = intptr; //虹軟引擎指針
    DoSomething();       //虹軟人臉對(duì)比具體流程
  })
  th.Start();
  th.Join();
}

避坑方案2:引擎池法

.Net 4.0不僅引入了ThreadLocal<T>,也引入了ConcurrentQueue<T>。ConcurrentQueue<T>隊(duì)列是一個(gè)高效的線程安全的隊(duì)列,ConcurrentQueue<T>數(shù)據(jù)結(jié)構(gòu)的示意圖:

image

由于ConcurrentQueue是線程安全隊(duì)列,我們不妨將引擎指針I(yè)ntPtr變量放在里面,避免多線程情景被同時(shí)取用,用不需要手動(dòng)在加鎖,豈不美哉!思路如下(源碼: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);
}

實(shí)際使用

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())
    {
    //識(shí)別業(yè)務(wù)
          byte[] featureByte=requestStream.Current.FaceFeature.ToByteArray();
          if(featureByte.Length!=1032) //注意,2.x和3.x版本的人臉特征長(zhǎng)度是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=$"對(duì)比異常 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);
  }

除了線程調(diào)用引起的內(nèi)存沖突外,引擎數(shù)量過多引起的內(nèi)存溢出也是多線程情況下的一大痛點(diǎn)。引擎過少處理效率不足,引擎多了,出現(xiàn)程序崩潰。引擎數(shù)量如何規(guī)劃?

避坑方案3:合理規(guī)劃線程數(shù)量(引擎并發(fā)數(shù)量)

虹軟的文檔中友善的提示了大家,引擎的數(shù)量不超過系統(tǒng)內(nèi)核數(shù)量(當(dāng)然,我認(rèn)為這建議比較保守,畢竟不同代數(shù),不同檔次的CPU的性能差別很大)。 對(duì)于初始化引擎數(shù)量,筆者根據(jù)自己的工程實(shí)踐,做了以下總結(jié)供大家參考:

  1. CPU方面:引擎數(shù)量根據(jù)系統(tǒng)CPU消耗情況估算,多個(gè)引擎同時(shí)處理時(shí),CPU占有率不超過90%。
  2. 內(nèi)存方面:每個(gè)引擎的內(nèi)存消耗按400M估算(以3代SDK為例,其他版本可自行測(cè)試估算),系統(tǒng)內(nèi)存占用不超過80%(注意:需要規(guī)劃預(yù)留圖片處理是需要的內(nèi)存?。?/li>
  3. 引擎數(shù)量取 CPU,內(nèi)存限制數(shù)量中的最小值。
  4. 容器化部署盡量避免一個(gè)程序一個(gè)引擎(浪費(fèi)注冊(cè)碼,增加資源消耗),但建議部署2,3個(gè)多引擎容器組成集群,并對(duì)每個(gè)容器做內(nèi)存資源CPU限制,平衡穩(wěn)定性,限制動(dòng)態(tài)擴(kuò)展容器數(shù)量。

總結(jié)

以上幾點(diǎn)避坑建議是筆者從工程實(shí)踐中得到的一點(diǎn)思考,歡迎更多的小伙伴嘗試使用虹軟SDK,對(duì)多線程方案有更好的建議,歡迎留言。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容