我在.NET 4.5.2上创建的MVC 5 ASP.NET应用程序上有一个服务层项目,它调用外部第三方WCF服务来异步获取信息。调用外部服务的原始方法如下(其中有3个类似于我从GetInfoFromExternalService方法按顺序调用的总数(注意它实际上并没有调用它 - 只是为了说明而命名)
private async Task<string> GetTokenIdForCarsAsync(Car[] cars)
{
try
{
if (_externalpServiceClient == null)
{
_externalpServiceClient = new ExternalServiceClient("WSHttpBinding_IExternalService");
}
string tokenId= await _externalpServiceClient .GetInfoForCarsAsync(cars).ConfigureAwait(false);
return tokenId;
}
catch (Exception ex)
{
//TODO plug in log 4 net
throw new Exception("Failed" + ex.Message);
}
finally
{
CloseExternalServiceClient(_externalpServiceClient);
_externalpServiceClient= null;
}
}
这意味着当每个异步调用完成finally块运行时 - WCF客户端关闭并设置为null,然后在另一个请求发出时进行新建。这个工作正常,直到需要进行更改,如果用户传入的汽车数量超过1000我创建一个拆分函数,然后在每个1000的WhenAll中调用我的GetInfoFromExternalService方法 - 如下所示:
if (cars.Count > 1000)
{
const int packageSize = 1000;
var packages = SplitCarss(cars, packageSize);
//kick off the number of split packages we got above in Parallel and await until they all complete
await Task.WhenAll(packages.Select(GetInfoFromExternalService));
}
然而现在这已经失败,好像我有3000辆汽车,方法调用WCF服务的GetTokenId消息,但finally块关闭它,所以第二批1000试图运行抛出异常。如果我删除了finally块,代码就可以正常运行 - 但显然不是关闭这个WCF客户端的好习惯。
我曾尝试将它放在我评估cars.count的if else块之后 - 但是如果用户上传例如2000辆汽车并且完成并且运行时间为1分钟 - 同时由于用户拥有控制权网页他们可以上传另一个2000或另一个用户可以上传,并再次与一个例外。
有没有人能够正确关闭外部服务客户端的好方法?
答案 0 :(得分:3)
基于你的the related question,你的“分裂”逻辑似乎没有给你你想要实现的目标。 WhenAll
仍然会并行执行请求,因此您可能会在任何给定时刻运行超过1000个请求。使用SemaphoreSlim
来限制同时活动的请求数,并将该数量限制为1000.这样,您就不需要进行任何拆分。
另一个问题可能是您如何处理ExternalServiceClient
客户端的创建/处置。我怀疑那里可能存在竞争条件。
最后,当您从catch
块重新抛出时,您至少应该包含对原始异常的引用。
以下是解决这些问题的方法(未经测试,但应该提供给您的想法):
const int MAX_PARALLEL = 1000;
SemaphoreSlim _semaphoreSlim = new SemaphoreSlim(MAX_PARALLEL);
volatile int _activeClients = 0;
readonly object _lock = new Object();
ExternalServiceClient _externalpServiceClient = null;
ExternalServiceClient GetClient()
{
lock (_lock)
{
if (_activeClients == 0)
_externalpServiceClient = new ExternalServiceClient("WSHttpBinding_IExternalService");
_activeClients++;
return _externalpServiceClient;
}
}
void ReleaseClient()
{
lock (_lock)
{
_activeClients--;
if (_activeClients == 0)
{
_externalpServiceClient.Close();
_externalpServiceClient = null;
}
}
}
private async Task<string> GetTokenIdForCarsAsync(Car[] cars)
{
var client = GetClient();
try
{
await _semaphoreSlim.WaitAsync().ConfigureAwait(false);
try
{
string tokenId = await client.GetInfoForCarsAsync(cars).ConfigureAwait(false);
return tokenId;
}
catch (Exception ex)
{
//TODO plug in log 4 net
throw new Exception("Failed" + ex.Message, ex);
}
finally
{
_semaphoreSlim.Release();
}
}
finally
{
ReleaseClient();
}
}
根据评论更新:
外部WebService公司可以接受我最多5000辆汽车 一次调用中的对象 - 尽管他们建议分成几批 1000并且一次最多并行运行5个 - 所以当我提到7000时 - 我不是说GetTokenIdForCarAsync会被调用7000次 - 我的代码目前应该被调用7次 - 即给我回来7次 令牌ID - 我想知道我可以使用你的信号量苗条先跑 5并联然后2
变化很小(但未经测试)。第一:
const int MAX_PARALLEL = 5;
然后,使用Marc Gravell的ChunkExtension.Chunkify
,我们会介绍GetAllTokenIdForCarsAsync
,而GetTokenIdForCarsAsync
将从上方调用private async Task<string[]> GetAllTokenIdForCarsAsync(Car[] cars)
{
var results = new List<string>();
var chunks = cars.Chunkify(1000);
var tasks = chunks.Select(chunk => GetTokenIdForCarsAsync(chunk)).ToArray();
await Task.WhenAll(tasks);
return tasks.Select(task => task.Result).ToArray();
}
:
GetAllTokenIdForCarsAsync
现在您可以将所有7000辆车传递到{{1}}。这是一个骨架,如果任何批处理请求失败,可以通过一些重试逻辑进行改进(我将其留给您)。