我在 .NET 4.5.2 上创建的 MVC 5 ASP.NET 应用程序上有一个服务层项目,该项目调用外部第 3 方 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 block 就会运行 - WCF 客户端被关闭并设置为 null,然后在发出另一个请求时更新。这工作正常,直到需要进行更改,如果用户传入的汽车数量超过 1000 辆,我创建一个 Split 函数,然后在 WhenAll 中调用我的 GetInfoFromExternalService 方法,每个 1000 辆 - 如下所示:
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 辆车一样,调用 GetTokenId 的方法将 WCF 服务更新为消息,但 finally block 将其关闭,因此尝试运行的第二批 1000 辆车会抛出异常。如果我删除 finally block ,代码可以正常工作 - 但不关闭此 WCF 客户端显然不是一个好习惯。
我曾尝试将其放在评估 cars.count 的 if else block 之后 - 但如果用户上传了例如 2000 辆汽车,并且在 1 分钟内完成并运行 - 同时用户可以控制他们可以上传另外 2000 个网页,或者其他用户可以上传,但它会再次因异常而失败。
是否有任何人都可以看到的正确关闭外部服务客户端的好方法?
最佳答案
基于the related question对于你来说,你的“ split ”逻辑似乎并没有给你想要实现的目标。 WhenAll
仍然并行执行请求,因此您最终可能会在任何给定时间运行超过 1000 个请求。使用 SemaphoreSlim
来限制同时事件请求的数量,并将该数量限制为 1000。这样,您就不需要进行任何拆分。
另一个问题可能是如何处理 ExternalServiceClient
客户端的创建/处置。我怀疑那里可能存在竞争条件。
最后,当您从 catch
block 重新抛出时,您至少应该包含对原始异常的引用。
以下是解决这些问题的方法(未经测试,但应该可以为您提供想法):
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();
}
}
根据评论更新:
the External WebService company can accept me passing up to 5000 car objects in one call - though they recommend splitting into batches of 1000 and run up to 5 in parallel at one time - so when I mention 7000 - I dont mean GetTokenIdForCarAsync would be called 7000 times - with my code currently it should be called 7 times - i.e giving me back 7 token ids - I am wondering can I use your semaphore slim to run first 5 in parallel and then 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();
}
现在您可以将所有 7000 辆车传递到 GetAllTokenIdForCarsAsync
中。这是一个框架,如果任何批处理请求失败,可以通过一些重试逻辑对其进行改进(我将其留给您)。
关于c# - 从异步方法关闭 WCF 服务?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/23770657/