【问题标题】:Async Download and Deserialize异步下载和反序列化
【发布时间】:2016-05-21 23:53:46
【问题描述】:

晚上好,

我正在尝试使用异步编程来优化一些代码,想知道以下代码是否写得好,是否有任何改进方法。

它的目的是从给定的 URL 下载一些 JSON 并将其反序列化为一个对象。

我有 3 个(现在是 4 个)问题(帖子末尾描述了 1 个问题):

  • 我应该使用TaskEx.Run 运行吗 Newtonsoft.Json.JsonConvert.DeserializeObject<T>?
  • 有什么好的方法(不检查对象属性) 知道rootObject是否创建成功?
  • 我是否应该在某处检查是否要求取消?
  • 新问题)我应该在发出新请求之前取消挂起请求吗?

废话不多说,代码如下:

internal static class WebUtilities
{
    /// <summary>
    /// Downloads the page of the given url
    /// </summary>
    /// <param name="url">url to download the page from</param>
    /// <param name="cancellationToken">token to cancel the download</param>
    /// <returns>the page content</returns>
    internal static async Task<string> DownloadStringAsync(string url, CancellationToken cancellationToken)
    {
        try
        {
            // create Http Client and dispose of it even if exceptions are thrown (same as using finally statement)
            using (var client = new HttpClient() {Timeout = TimeSpan.FromSeconds(5)})
            {
                // should I always do this?
                client.CancelPendingRequests();

                // do request and dispose of it when done
                using (var response = await client.GetAsync(url, cancellationToken).ConfigureAwait(false))
                {
                    // if response was successful (otherwise it will return null)
                    if (response.IsSuccessStatusCode)
                    {
                        // return its content
                        return await response.Content.ReadAsStringAsync().ConfigureAwait(false);
                    }
                }
            }
        }
        catch (Exception ex) when (ex is System.Net.Sockets.SocketException || 
                                    ex is InvalidOperationException || 
                                    ex is OperationCanceledException ||
                                    ex is System.Net.Http.HttpRequestException)
        {
            WriteLine("DownloadStringAsync task has been cancelled.");
            WriteLine(ex.Message);
            return null;
        }

        // return null if response was unsuccessful
        return null;
    }

    /// <summary>
    /// Downloads Json from a given url and attempts its deserialization to a given reference type (class)
    /// </summary>
    /// <typeparam name="T">the class to deserialize to</typeparam>
    /// <param name="url">url to download the json from</param>
    /// <param name="cancellationToken">token to cancel the download</param>
    /// <returns>the deserialized object</returns>
    internal static async Task<T> DownloadJsonAndDeserialize<T>(string url, CancellationToken cancellationToken) where T : class, new()
    {
        // download json from the given url
        var jsonResponse = await DownloadStringAsync(url, cancellationToken).ConfigureAwait(false);

        // if the response is invalid, no need to go further
        if (string.IsNullOrEmpty(jsonResponse))
            // return a default constructor instance
            return new T();

        // try to deserialize
        try
        {
            // Deserialize json data to the given .NET object
            // Should I use TaskEx.Run here?
            return Newtonsoft.Json.JsonConvert.DeserializeObject<T>(jsonResponse);
        }
        catch (Exception ex) when (ex is JsonException)
        {
            WriteLine("Something went wrong while deserializing json to the given reference type.");
            WriteLine(ex.Message);
        }

        // return a default constructor instance
        return new T();
    }
}

要调用代码,可以执行以下操作:

internal static async Task CallAsync()
    {
        RootObject root = null;

        using (var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10)))
        {
            var token = cts.Token;

            root = await WebUtilities.DownloadJsonAndDeserialize<RootObject>(URL, token).ConfigureAwait(false);
        }

        // do something with rootObject
        // any good way to know if rootObject was successfully created, before moving on?
    }

你会改变什么?为什么?

谢谢!

编辑:

  • @codran 建议 - 每个await 现在都在使用ConfigureAwait(false)
  • CancellationTokenSource 现已处理(使用语句)
  • @codran 建议 - 现在过滤了一些例外情况
  • DownloadStringAsync 现在在 try-catch 中有 2 个 using 语句 块以提高可读性(而不是 try-catch-finally)。
  • 现在检查是否响应IsSuccessStatusCode

发现问题(为此创建了另一个问题 - Timeouts in Xamarin HTTP requests):

有趣的是,当一个主机无法访问(例如,离线本地服务器)时,GetAsync 之后什么都没有发生几分钟后(大约 3 分钟)一个System.Net.WebException 被抛出,说Error: ConnectFailure (Connection timed out)。内部异常是System.Net.Sockets.SocketsException(此处为完整日志:http://pastebin.com/MzHyp2FM)。

我尝试将client.Timeout 设置为 5 秒但这似乎不起作用

但可能是 Xamarin 错误 (https://forums.xamarin.com/discussion/5941/system-net-http-httpclient-timeout-seems-to-be-ignored)。

不管怎样,cancellationToken 不应该在 10 秒后自动取消

此超时问题发生在/何时:

  • 脱机/无法访问的 IP 地址(在我的情况下是 请求离线本地服务器,例如 192.168.1.101:8080)(例如,GetAsync、SendAsync、GetResponseAsync)

代码适用于/何时:

  • 没有 Internet 连接(抛出异常)
  • DNS 无法解析 URL(引发异常)
  • 提供了一个有效的 URL(正常运行)

  • 请求来自桌面客户端(例如 WPF),如果 IP 离线/无法访问它会很快抛出 4 个异常(否 可以建立连接,因为目标机器主动拒绝 它

结论

  • Xamarin 似乎在这些请求中有一些错误(超时在 至少?),因为它们没有给出预期的结果,否则在 桌面应用程序。

【问题讨论】:

  • HttpClient.TimeoutCancellationToken 应该都可以正常工作。 Xamarin 最有可能出现的错误(我见过 很多 具有异步和并行支持)。 Xamarin 正在迁移到 .NET Core,这将极大地提高它们的可靠性和正确性。国际海事组织。 :)
  • 感谢@StephenCleary 光临!不幸的是,情况似乎如此(自 2012 年或 2013 年以来?)。还有什么我可以改进的吗?我的第一个问题怎么样?
  • 不,我不会使用 Task.Run 进行 JSON 反序列化。除非您的对象 大量,否则它们应该快速反序列化,即使它们 大量,Task.Run 也属于 UI 级别,而不是 WebUtilities 模块。
  • @StephenCleary 完全有道理。我问是因为我看到了一些使用 Task.RunEx 进行 JSON 反序列化的示例。仅供参考,刚刚用一些测试结果更新了主帖......似乎这确实是 Xamarin 中的一个错误,因为它在桌面客户端应用程序中完美运行。我真的很难过...但很高兴代码有效。我能做什么?
  • 当然,只是关于 Xamarin HTTP 请求的超时。

标签: c# asynchronous xamarin.android


【解决方案1】:

我应该使用TaskEx.Run 来运行Newtonsoft.Json.JsonConvert.DeserializeObject&lt;T&gt;吗?

将 JSON 反序列化扔到另一个线程可能不会取得任何成果。您正在释放当前线程,但随后需要等待您的工作被安排在另一个线程上。除非您特别需要 Newtonsoft.Json API,否则请考虑使用 ReadAsAsync&lt;T&gt;。您可以将其与JsonMediaTypeFormatter 一起使用,其中uses Newtonsoft.Json internally anyway

有什么好方法(不检查对象属性)知道rootObject是否创建成功?

您需要定义成功的样子。例如,您的 JSON 可能是 null,因此 rootObject 为空。或者它可能缺少属性,或者您的 JSON 具有未反序列化的额外属性。我认为没有办法使序列化失败,因为多余或缺少的属性,所以你必须自己检查。在这一点上我可能是错的。

我是否应该在某处检查是否要求取消?

您需要了解代码中的意义以及何时取消。例如,当所有工作都已完成时,将取消标记作为函数的最后一行进行操作是没有意义的。

如果 JSON 已下载但尚未反序列化,您的应用程序取消操作是否有意义?如果 JSON 已反序列化,但对象图尚未验证(问题 2),您的应用程序取消操作是否有意义?

这确实取决于您的需求,但如果下载尚未完成,我可能会取消 - 似乎您已经在这样做 - 但一旦下载完成,那就是不归路。

你会改变什么?为什么?

在您await 的任务上使用ConfigureAwait(false)。如果您的代码曾经阻塞并等待生成的任务(例如 .Wait().Result),这可以防止死锁。

使用using 块而不是try-finally

【讨论】:

  • 还有一件事 - 不要抓住Exception。捕获特定的子类,例如OperationCanceledException.
  • ConfigureAwait(false) 不是灵丹妙药,经常使用错误的东西,当然也不是解决死锁的方法。死锁的答案是不要让线程等待以不同顺序获取的多个锁。
  • @PeterDuniho 我发现通常是正确的 UI 功能除外。此外,死锁并不总是与锁相关的。如果调度程序正在等待一个线程,而该线程又在等待调度的任务,则不涉及显式锁,但存在死锁。
  • @Apidcloud TaskCanceledExceptionOperationCanceledException 的子类。在这种情况下,您不需要同时捕获两者。 GetAsync 不应无限期挂起,IIRC 的默认超时时间为 60 秒。您将什么定义为“无法访问”?有多种故障类型可能导致连接失败。
  • @Apidcloud 您只会捕获预期的异常,并且在这种情况下您有比允许异常处理程序接管更具体的处理方法。您希望将不可预见的异常冒泡到异常处理程序的更高位置,以便它可以记录异常、通知您并阻止代码继续执行、做出不正确的假设并破坏重要的事情。
【解决方案2】:

我在生产中使用以下代码没有任何问题。

// Creating and disposing HttpClient is unnecessary
// if you are going to use it multiple time
// reusing HttpClient improves performance !!
// do not worry about memory leak, HttpClient
// is designed to close resources used in single "SendAsync"
// method call
private static HttpClient client;


public Task<T> DownloadAs<T>(string url){

    // I know I am firing this on another thread
    // to keep UI free from any smallest task like
    // preparing httpclient, setting headers
    // checking for result or anything..., why do that on
    // UI Thread?

    // this helps us using excessive logging required for
    // debugging and diagnostics

    return Task.Run(async ()=> {

       // following parses url and makes sure
       // it is a valid url
       // there is no need for this to be done on 
       // UI thread
       Uri uri = new Uri(url);

       HttpRequestMessage request = 
          new HttpRequestMessage(uri,HttpMethod.Get);

       // do some checks, set some headers...
       // secrete code !!!

       var response = await client.SendAsync(request,
           HttpCompletionOption.ReadHeaders);

       string content = await response.Content.ReadAsStringAsync();

       if(((int)response.StatusCode)>300){

           // it is good to receive error text details
           // not just reason phrase

           throw new InvalidOperationException(response.ReasonPhrase  
              + "\r\n" + content);
       }
       return JsonConvert.DeserializeObject<T>(content);

    });
}

ConfigureAwait 很棘手,并且使调试更加复杂。此代码将毫无问题地运行。

如果您想使用CancellationToken,那么您可以在client.GetAsync 方法中传递您的令牌。

【讨论】:

  • 有什么理由不使用client.GetAsyncresponse.Content.ReadAsStringAsync
  • @Apidcloud 抱歉,打错了,添加了GetAsync
  • 我仍在努力让您的代码正常工作。 HttpRequestMessage 构造函数应该有 uri 作为第二个参数。但是GetAsync 似乎没有HttpRequestMessage 过载。
  • 如果我使用 GetAsync(uri, cancelToken) 仍然需要大约 3 分钟才能抛出上述异常。
  • 注意到您已更改为 SendAsync,但超时问题仍然存在。设置 client.Timeout 似乎不起作用。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2016-07-22
  • 1970-01-01
  • 1970-01-01
  • 2014-11-15
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多