多个异步调用,如何以有意义的方式处理响应

Posted

技术标签:

【中文标题】多个异步调用,如何以有意义的方式处理响应【英文标题】:Multiple Async calls, how to deal with responses in a way that make sense 【发布时间】:2022-01-19 02:14:01 【问题描述】:

我正在对几个不同的 URL 进行多次异步调用,两个 url 应该返回相同的结果,但我想比较两者的结果或检查响应中的某些值。我不确定如何在状态代码之外的响应中比较或查找特定值,有没有简单的方法可以做到这一点?还想注意响应,如果失败了,我希望稍后能够在我的代码中跟踪它,以便不再使用该 url,我不确定我将如何处理这个

代码:

private async Task<ClientModel> getClientInfoAsync(string clientID)
    
        
        ClientModel c = null;
       
        try
        
            
            var client = new HttpClient();
            //Start requests for all of them
            var requests = urls.Select
                (
                url => client.GetAsync(getURL(url, "Client", clientID))
                ).ToList();
            //Wait for all the requests to finish
            await Task.WhenAll(requests);

            //Get the responses
            var responses = requests.Select
                (
                    task => task.Result
                );
           
            foreach (var r in responses)
            
                
                // Extract the message body
                var s = await r.Content.ReadAsStringAsync();                    
                          
                if (r.IsSuccessStatusCode)
                
                    c = r.Content.ReadAsAsync<ClientModel>().Result;                        
                    SetLastSuccessfulCommunicationDetails();  //after this call HERE I THINK IS WHERE I WOULD COMPARE RESPONSES AND GO FROM THERE                     

                
                
            
           
        
        catch (Exception ex)
        
            string errMsg = "Error getting the client info";
            //...catch error code here...
        
        
        return c;
    

基本上我不确定如何处理响应,并且根据我的比较和响应状态仅返回一个客户端模型 (c)。让我知道是否需要包含任何进一步的信息

【问题讨论】:

“几个不同的 URL”:这是否意味着你知道你正在处理两个 URL,或者会有多个 URL 相互比较?您希望进行什么样的比较? 我不确定是否有人可以为您回答这个问题。反应有多相似或不同?响应的顺序重要吗?如果所有回复都“同意”,那么您将使用哪个个人回复作为结果的基础有什么关系。 现在它只是两个 url,但它可以更晚,只要响应相似或不同,它们应该在失败之外返回相同的响应,因为 url 向下返回失败。我想我只是想验证他们返回相同的结果的机会,他们没有,至少记录差异,希望澄清一点 还要澄清一点,如果响应同意你是对的,我不在乎我使用哪个响应,我想更多地使用它,好像 URL #2 响应失败原因或其他,但#1是成功我想保留#1结果,后来不使用URL#2,因为它失败了这个任务并且任何进一步的对该URL的调用都不会返回成功,因为这个初始失败。我想我不确定我是否需要担心异步调用是否失败,并且总是只调用两个 URL,因为这样做的时间无关紧要 @Enigmativity 是的,我认为大部分情况下都可以 【参考方案1】:

如果我可以假设你有一个看起来像这样的方法:

private Task<ClientModel> DetermineClientModelFromResponses(IEnumerable<string> responses)

...那么您可以使用 Microsoft 的 Reactive Framework (aka Rx) - NuGet System.Reactive 并添加 using System.Reactive.Linq;

它让你这样做:

private async Task<ClientModel> GetClientInfoAsync(string clientID) =>
    await DetermineClientModelFromResponses(
        await Observable.Using(
            () => new HttpClient(),
            client =>
                urls
                    .ToObservable()
                    .SelectMany(url => Observable.FromAsync(() => client.GetAsync(getURL(url, "Client", clientID))))
                    .Where(response => response.IsSuccessStatusCode)
                    .SelectMany(response => Observable.FromAsync(() => response.Content.ReadAsStringAsync()))
                    .ToArray()));
                

...或者,或者,这个:

private async Task<ClientModel> GetClientInfoAsync(string clientID) =>
    await DetermineClientModelFromResponses(
        await Observable.Using(
            () => new HttpClient(),
            client =>
            (
                from url in urls.ToObservable()
                from response in Observable.FromAsync(() => client.GetAsync(getURL(url, "Client", clientID)))
                where response.IsSuccessStatusCode
                from text in Observable.FromAsync(() => response.Content.ReadAsStringAsync())
                select text
            ).ToArray()));

如果您对第一个成功响应获胜感到满意,那么这应该适合您,但您需要确保您至少有一个成功:

private async Task<ClientModel> GetClientInfoAsync(string clientID) =>
    await Observable.Using(
        () => new HttpClient(),
        client =>
        (
            from url in urls.ToObservable()
            from response in Observable.FromAsync(() => client.GetAsync(getURL(url, "Client", clientID)))
            where response.IsSuccessStatusCode
            from text in Observable.FromAsync(() => response.Content.ReadAsAsync<ClientModel>())
            select text
        ).Take(1));

为了使这对错误更加稳健,您有一些策略。

我把你的代码改写成一个简单的例子:

async Task Main()

    var result = (string)"no result";
    try
    
        result = await GetClientInfoAsync("123");
    
    catch (NotImplementedException ex)
    
        Console.WriteLine(ex.Message);
    
    Console.WriteLine(result);


private List<string> urls = new List<string>()  "Hello" ;

private async Task<string> GetClientInfoAsync(string clientID) =>
    await Observable.Using(
        () => new HttpClient(),
        client =>
        (
            from url in urls.ToObservable()
            from response in Observable.FromAsync(() => Test1(url))
            from text in Observable.FromAsync(() => Test2(response))
            select $"clientID:text"
        )
            .Concat(Observable.Return<string>(null))
            .Take(1));

private Random _random = new Random();

Task<string> Test1(string url)

    if (_random.NextDouble() > 0.3)
    
        throw new NotImplementedException("Test1!");
    
    return Task.Run(() => $"url!");


Task<string> Test2(string response)

    if (_random.NextDouble() > 0.3)
    
        throw new NotImplementedException("Test2!");
    
    return Task.Run(() => $"response#");

一旦出现异常,此代码将结束 GetClientInfoAsync,并让它冒泡到 Main 方法。这对你来说可能还不够。

另一种方法是向每个Test1Test2 添加正常的try/catch 代码,以确保它们永远不会失败。

或者,您可以很容易地添加“重试”功能。

private async Task<string> GetClientInfoAsync(string clientID) =>
    await Observable.Using(
        () => new HttpClient(),
        client =>
        (
            from url in urls.ToObservable()
            from response in Observable.Defer(() => Observable.FromAsync(() => Test1(url))).Retry(5)
            from text in Observable.Defer(() => Observable.FromAsync(() => Test2(response))).Retry(5)
            select $"clientID:text"
        )
            .Concat(Observable.Return<string>(null))
            .Take(1));

请注意,现在 Test1Test2 各重试 5 次。

错误仍有可能通过,但这是正常编码,对吗?

请注意,我还添加了.Concat(Observable.Return&lt;string&gt;(null)),以确保如果查询本身没有值,则查询会生成一个值。 Concat 在连接 null 结果之前等待主查询结束,所以如果主查询没有产生任何值,那么 null 就会出来。

【讨论】:

我真的很喜欢这特别是第一次成功响应的紧凑程度,我有一个问题是我是否能够对此进行尝试捕获,如果没有至少一次成功catch 会被使用吗?感谢您的回复/cmets @Ledz3p - 我删除了一些错误处理代码供您查看。【参考方案2】:

首先,如果可以的话,不要使用.Result。有一种更简洁的方式来获取所有响应。

var responses = await Task.WhenAll(requests);

这种方法的一个缺点是,如果任何请求引发异常,它都会引发异常。因此,如果您想智能地处理这种情况,您需要尝试/捕获并有一些特殊的逻辑来处理它。

您可以为每个异步步骤继续该模式,将您的结果作为集合处理:

        var validResponses = responses.Where(r =>r.IsSuccessStatusCode);
        var clientModels = await Task.WhenAll(
            validResponses
                .Select(async r => await r.Content.ReadAsAsync<ClientModel>()));

这种方法的一个缺点是您最终要等待所有请求完成,然后才能开始阅读任何响应内容。因此,将发出请求、获取响应以及沿途收集任何数据的整个过程放在一个异步方法中可能更有意义,只需说var responseData = await Task.WhenAll(urls.Select(ThatMethod))。这样一来,只要有一个请求返回,您就可以开始使用它的响应。

在比较结果时,如果不知道您在寻找哪种比较,就无法提供帮助。但是,假设您想要模型中某些属性值最高的那个,例如:

        var lastResponseModel = clientModels
            .OrderByDescending(c => c.LastSaved)
            .First();
        return lastResponseModel;

【讨论】:

我喜欢你提到的单一异步方法的想法var responseData = await Task.WhenAll(urls.Select(ThatMethod)) 然后我仍然在 ResponseData 中做一个 foreach 来读取内容吗?这是我唯一不确定的部分。而且你的比较也很有帮助,我想我需要弄清楚我想要比较什么,并且像房产的最高价值这样的东西是一个好的开始

以上是关于多个异步调用,如何以有意义的方式处理响应的主要内容,如果未能解决你的问题,请参考以下文章

如何在多个异步调用中向 NodeJS 中的数据库发送响应?

Javascript - 异步调用后同步

如何从异步调用返回响应

如何从异步调用返回响应

如何从异步调用返回响应

如何从异步调用返回响应