虹软开放人脸识别SDK以来,成功把人脸识别技术拉下神台,几乎所有开发者可以“0成本”使用到人脸(证)到其项目。但在官方论坛,QQ群,微信群等平台,很多初学者对如何在多线程下使用产生疑惑,掉入坑中(尤其是没有C++的基础的C#开发)。今天,分享两种.net (core)下的多线程使用方式,贡大家探讨。大家有更好的方式,也可以积极留言交流。
先分析问题来源,为什么C#的一般多线程调用方式容易产生错误,尤其是“尝试写保护内存”的错误。原因是C#开发使用的虹软的算法SDK均为C++版本(Windows/Linux),C++作为线程不安全的程序,可以直接操作内存。多个线程同时调用一个引擎,就是同时对一段内存操作,产生内存错误,程序崩溃。
解决方案一:基于ThreadLocal 强制一个线程捆绑一个引擎。
ThreadLocal的主要作用是让各个线程维持自己的变量。ThreadLocal 是线程的局部变量, 是每一个线程所单独持有的,其他线程不能对其进行访问, 通常是类中的 private static 字段。当使用ThreadLocal维护变量的时候 为每一个使用该变量的线程提供一个独立的变量副本,即每个线程内部都会有一个该变量,这样同时多个线程访问该变量并不会彼此相互影响,因此他们使用的都是自己从内存中拷贝过来的变量的副本, 这样就不存在线程安全问题,也不会影响程序的执行性能。在虹软人脸的具体应用中,毫无疑问,把初始化好的引擎指针(C#中的Intptr类型)赋值给线程的Threadlocal,就可以开心的玩耍了。找个网上的code演示下:(本人基于ThreadLocal 的工程找不到了)
static void Main()
{
var local = new ThreadLocal
//修改TLS的线程
Thread th = new Thread(() =>
{
local.Value = intptr; //虹软引擎指针
DoSomething(); //虹软人脸对比具体流程
})
th.Start();
th.Join();
}
解决方案二:基于“引擎池”实现多线程与高并发。
相比于方案一,我更喜欢“引擎池”,应为它更方便灵活,还更适合.net core Web Api这样的后端框架。废话不说,代码伺候:
1. 定义"引擎池"接口 (由于我方业务需要,初始化了3个不同的引擎池,相关的引擎参数不相同)
public interface IEnginePoor
{
public ConcurrentQueue
public ConcurrentQueue
public ConcurrentQueue
public IntPtr GetEngine(ConcurrentQueue
public void PutEngine(ConcurrentQueue
}
2. 实现相关接口 (Arcsoft_Face_3_0 为虹软dll的C#封装)
public class Arcsoft_Face_Action : Arcsoft_Face_3_0, IEnginePoor
{
public string AppID { get; }
public string AppKey { get; }
public int FaceEngineNums { get; set; }
public int IDEngineNums { get; set; }
public int AIEngineNums { get; set; }
public ConcurrentQueue
public ConcurrentQueue
public ConcurrentQueue
private int InitEnginePool()
{
try
{
for (int index = 0; index < FaceEngineNums; index++)
{
IntPtr enginePtr = IntPtr.Zero;
Arcsoft_Face_Action faceAction = new Arcsoft_Face_Action(AppID, AppKey);
enginePtr = faceAction.InitASFEnginePtr(ParmsBestPractice.faceBaseMask);
PutEngine(FaceEnginePoor, enginePtr);
Console.WriteLine($"FaceEnginePoor add {enginePtr}");
}
for (int index = 0; index < IDEngineNums; index++)
{
IntPtr enginePtr = IntPtr.Zero;
Arcsoft_Face_Action faceAction = new Arcsoft_Face_Action(AppID, AppKey);
enginePtr = faceAction.InitASFEnginePtr(ParmsBestPractice.faceBaseMask);
PutEngine(IDEnginePoor, enginePtr);
Console.WriteLine($"IDEnginePoor add {enginePtr}");
}
for (int index = 0; index < AIEngineNums; index++)
{
IntPtr enginePtr = IntPtr.Zero;
int aiMask = FaceEngineMask.ASF_AGE | FaceEngineMask.ASF_GENDER | FaceEngineMask.ASF_FACE3DANGLE | FaceEngineMask.ASF_LIVENESS;
Arcsoft_Face_Action faceAction = new Arcsoft_Face_Action(AppID, AppKey);
enginePtr = faceAction.InitASFEnginePtr(ParmsBestPractice.faceBaseMask | aiMask);
PutEngine(AIEnginePoor, enginePtr);
Console.WriteLine($"AIEnginePoor add {enginePtr}");
}
return 0;
}
catch (Exception ex)
{
throw new Exception($"InitEnginePool--> exception {ex}");
}
}
public IntPtr GetEngine(ConcurrentQueue
{
IntPtr item = IntPtr.Zero;
if (queue.TryDequeue(out item))
{
return item;
}
else
{
return IntPtr.Zero;
}
}
public void PutEngine(ConcurrentQueue
{
if (item != IntPtr.Zero)
{
queue.Enqueue(item);
}
}
public void Arcsoft_EnginePool(int faceEngineNums , int idEngineNums , int aiEngineNums)
{
FaceEnginePoor = new ConcurrentQueue
IDEnginePoor = new ConcurrentQueue
AIEnginePoor = new ConcurrentQueue
try
{
FaceEngineNums = faceEngineNums;
IDEngineNums = idEngineNums;
AIEngineNums = aiEngineNums;
int status = InitEnginePool();
if (status != 0)
{
throw new Exception("引擎池初始化失败!");
}
}
catch (Exception ex)
{
throw new Exception($"ArcSoft_EnginePool-->ArcSoft_EnginePool exception as: {ex}");
}
}
private int InitEnginePool()
{
try
{
for (int index = 0; index < FaceEngineNums; index++)
{
IntPtr enginePtr = IntPtr.Zero;
Arcsoft_Face_Action faceAction = new Arcsoft_Face_Action(AppID, AppKey);
enginePtr = faceAction.InitASFEnginePtr(ParmsBestPractice.faceBaseMask);
PutEngine(FaceEnginePoor, enginePtr);
Console.WriteLine($"FaceEnginePoor add {enginePtr}");
}
for (int index = 0; index < IDEngineNums; index++)
{
IntPtr enginePtr = IntPtr.Zero;
Arcsoft_Face_Action faceAction = new Arcsoft_Face_Action(AppID, AppKey);
enginePtr = faceAction.InitASFEnginePtr(ParmsBestPractice.faceBaseMask);
PutEngine(IDEnginePoor, enginePtr);
Console.WriteLine($"IDEnginePoor add {enginePtr}");
}
for (int index = 0; index < AIEngineNums; index++)
{
IntPtr enginePtr = IntPtr.Zero;
int aiMask = FaceEngineMask.ASF_AGE | FaceEngineMask.ASF_GENDER | FaceEngineMask.ASF_FACE3DANGLE | FaceEngineMask.ASF_LIVENESS;
Arcsoft_Face_Action faceAction = new Arcsoft_Face_Action(AppID, AppKey);
enginePtr = faceAction.InitASFEnginePtr(ParmsBestPractice.faceBaseMask | aiMask);
PutEngine(AIEnginePoor, enginePtr);
Console.WriteLine($"AIEnginePoor add {enginePtr}");
}
return 0;
}
catch (Exception ex)
{
throw new Exception($"InitEnginePool--> exception {ex}");
}
}
}
3. 实现CustomServiceCollection 方便依赖注入
public static class CustomServiceCollection
{
public static IServiceCollection AddArcSoftFaceService(this IServiceCollection services, Arcsoft_Face_Action enginePool)
{
services.AddSingleton
return services;
}
}
4. 在Startup里面添加“虹软”Service。(同时推荐搭配Microsoft.AspNetCore.ConcurrencyLimiter中间件,限制并发量,以免内存不足)
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
services.AddControllers();
//用于传入的请求进行排队处理,避免线程池的不足.
services.AddQueuePolicy(options =>
{
//最大并发请求数 (建议与引擎数保持一直,虹软官方的说法是的最大引擎数不超过电脑的核数,我反正是不信的,难道志强和奔腾一样?内存足够大,我一般是和虚拟线程数一致,比如6核12线程,我就开12个引擎。)
options.MaxConcurrentRequests = faceEngineNums;
//请求队列长度限制
options.RequestQueueLimit = requestQueueLimit;
});
//添加虹软“引擎池”服务
Arcsoft_Face_Action enginePool = new Arcsoft_Face_Action(appID, faceKey);
enginePool.Arcsoft_EnginePool(faceEngineNums, 0, 0);
services.AddArcSoftFaceService(enginePool);
}
5. 在Controller里面实际使用。
public class FaceController : ControllerBase
{
public FaceController(IConfiguration configuration, IEnginePoor process)
{
Configuration = configuration;
FaceProcess = process;
float.TryParse(Configuration.GetSection("AppSettings:FaceMixLevel").Value, out faceMix);
int.TryParse(Configuration.GetSection("AppSettings:MaxProcessTime").Value, out maxProcessTime);
}
[HttpPost]
[Route("CompareTwoFaces")]
[DisableRequestSizeLimit]
public IActionResult CompareTwoFaces(IFormFile faceA, IFormFile faceB)
{
IntPtr engine = FaceProcess.GetEngine(FaceProcess.FaceEnginePoor);
CancellationTokenSource tokenSource = new CancellationTokenSource();
CustomResult faceResult = new CustomResult();
Tuple
Tuple
//调用引擎池逻辑!
var task = Task.Run(() =>
{
while (engine == IntPtr.Zero)
{
Task.Delay(10);
if (tokenSource.Token.IsCancellationRequested)
{
throw new Exception("等待引擎超时!");
}
engine = FaceProcess.GetEngine(FaceProcess.FaceEnginePoor);
}
using (var ms = new MemoryStream())
{
faceA.CopyTo(ms);
faceAResult = Arcsoft_Face_Action.TryExtractSingleFaceFeature(ms, 10, engine);
if (!faceAResult.Item1)
{
faceResult.Success = false;
faceResult.msg = faceAResult.Item3;
return;
}
}
using (var ms = new MemoryStream())
{
faceB.CopyTo(ms);
faceBResult = Arcsoft_Face_Action.TryExtractSingleFaceFeature(ms, 10, engine);
if (!faceBResult.Item1)
{
faceResult.Success = false;
faceResult.msg = faceBResult.Item3;
return;
}
}
float result = 0;
int compareStatus = Arcsoft_Face_3_0.ASFFaceFeatureCompare(engine, faceAResult.Item2, faceBResult.Item2, ref result, ASF_CompareModel.ASF_LIFE_PHOTO);
if (compareStatus == 0)
{
faceResult.Success = true;
faceResult.msg = $"相似度: {result} 接客引擎:{engine}";
}
else
{
faceResult.Success = false;
faceResult.msg = $"compareStatus error code = {compareStatus} 接客引擎:{engine}";
}
}, tokenSource.Token);
//响应时间控制
try
{
int timeLast = maxProcessTime * 1000;
while (timeLast > 0)
{
Task.Delay(100).Wait();
timeLast = timeLast - 100;
if (task.IsCompletedSuccessfully)
{
return Ok(JsonConvert.SerializeObject(faceResult));
}
}
tokenSource.Cancel();
return Ok(JsonConvert.SerializeObject(faceResult));
}
catch (Exception ex)
{
faceResult.Success = false;
faceResult.msg = ex.Message;
return Ok(JsonConvert.SerializeObject(faceResult));
}
finally
{
FaceProcess.PutEngine(FaceProcess.FaceEnginePoor, engine);
Marshal.FreeHGlobal(faceAResult.Item2);
Marshal.FreeHGlobal(faceBResult.Item2);
tokenSource.Dispose();
GC.Collect();
}
}
}
6. 结果演示:
后记:
多线程一般是为应对高并发的情形,本篇文章仅仅提供一种应对虹软人脸识别的多线程的简单初级处理方式,但对于真正的互联网级别的高并发,肯定是力不从心的。笔者在其他高并发项目中,还是基于k8s的redis+Kafka的分布式微服务架构处理,一个pod里面一个引擎(有点废号)。当然,Adp vNext也很香。对于私人开发者,每年100个注册码,k8s的消耗可能太大,本文的方式还是能节约一些注册码。