前言
多線程一直是后端開發(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)的示意圖:

由于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é)供大家參考:
- CPU方面:引擎數(shù)量根據(jù)系統(tǒng)CPU消耗情況估算,多個(gè)引擎同時(shí)處理時(shí),CPU占有率不超過90%。
- 內(nèi)存方面:每個(gè)引擎的內(nèi)存消耗按400M估算(以3代SDK為例,其他版本可自行測(cè)試估算),系統(tǒng)內(nèi)存占用不超過80%(注意:需要規(guī)劃預(yù)留圖片處理是需要的內(nèi)存?。?/li>
- 引擎數(shù)量取 CPU,內(nèi)存限制數(shù)量中的最小值。
- 容器化部署盡量避免一個(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ì)多線程方案有更好的建議,歡迎留言。