【问题标题】:Closing WCF Service from Async method?从异步方法关闭 WCF 服务?
【发布时间】:2014-07-09 08:32:39
【问题描述】:

我在 .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 块运行 - 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 辆汽车一样,对 GetTokenId 的方法调用通知了 WCF 服务,但 finally 块将其关闭,因此第二批尝试运行的 1000 辆汽车会引发异常。如果我删除 finally 块,代码可以正常工作 - 但不关闭此 WCF 客户端显然不是一个好习惯。

我曾尝试将它放在我的 if else 块之后评估 cars.count - 但是如果用户上传了例如 2000 辆汽车并且在 1 分钟内完成并运行 - 同时用户可以控制他们可以再上传 2000 个网页,或者其他用户可以上传,但它再次因异常而崩溃。

任何人都可以看到正确关闭外部服务客户端的好方法吗?

【问题讨论】:

    标签: c# .net wcf asynchronous async-await


    【解决方案1】:

    根据您的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();
        }
    }
    

    更新基于评论:

    External WebService 公司可以接受我传递多达 5000 辆汽车 一次调用中的对象-尽管他们建议将对象拆分为批次 1000 并且一次并行运行多达 5 个 - 所以当我提到 7000 - 我并不是说 GetTokenIdForCarAsync 会被调用 7000 次 - 我的代码目前应该被调用 7 次 - 即给我 7 令牌 ids - 我想知道我可以使用你的信号量 slim 先运行吗 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();
    }
    

    现在您可以将所有 7000 辆汽车传递到 GetAllTokenIdForCarsAsync。这是一个框架,如果任何批处理请求失败,可以通过一些重试逻辑来改进它(我把它留给你)。

    【讨论】:

    • 谢谢 - 我会试一试 - 对于 Semaphore Slim 部分 - 不确定上面是如何工作的 - 用户传入 7000 个汽车对象 - 我想将它们分成 1000 个包并运行并行 - 最好一次是 5 个 - 我是否应该仍然拥有我的 if cars.count > 1000 - 将它们分成 1000 个的包 - 使用 Tasks.WhenAll 但将 MAX_PARALLEL 设置为 5 的信号量苗条代码到位? (即)前 5 个 1000 包将并行运行 - 然后 2 个 1000 包将运行 - 然后 Task.WhenAll 将完成
    • @KOL,您可以立即并行启动 7000 个GetTokenIdForCarsAsync,然后在所有这些上执行await Task.WhenAll。尽管如此,上面的代码将确保同时运行的请求不超过 1000 个。当达到 1000 个并发请求的数量时,第 1001 个请求将在第一个请求完成时开始。这就是SemaphoreSlim 在这里工作的方式。您仍然可以将请求拆分为包,但您不必这样做。
    • 第一个 - 关闭 WCF 的代码似乎运行良好 :) - 虽然仍然不确定 Semaphore Slim - 也许我没有很好地解释它 - 外部 WebService 公司可以接受我传递给一次调用 5000 个汽车对象 - 尽管他们建议分成 1000 个批次并一次并行运行多达 5 个 - 所以当我提到 7000 - 我并不是说 GetTokenIdForCarAsync 会被调用 7000 次 - 我的代码目前应该被调用7 次 - 即给我 7 个令牌 id - 我想知道我可以使用你的信号量苗条来并行运行前 5 个,然后运行 ​​2 个
    • @KOL,现在我明白你的意思了。是的,你仍然可以使用SemaphoreSlim,我已经更新了代码来展示如何。
    猜你喜欢
    • 2014-04-30
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-05-22
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多