在按钮点击时调用异步方法

98

我创建了一个Windows Phone 8.1项目,并且正在尝试在按钮单击时运行async方法GetResponse<T>(string url),并等待该方法完成,但该方法从未完成。这是我的代码:

private void Button_Click(object sender, RoutedEventArgs 
{
      Task<List<MyObject>> task = GetResponse<MyObject>("my url");
      task.Wait();
      var items = task.Result; //break point here
}

public static async Task<List<T>> GetResponse<T>(string url)
{
    List<T> items = null;
    HttpWebRequest request = (HttpWebRequest)HttpWebRequest.Create(url);

    var response = (HttpWebResponse)await Task.Factory.FromAsync<WebResponse>(request.BeginGetResponse, request.EndGetResponse, null);
    try
    {
        Stream stream = response.GetResponseStream();
        StreamReader strReader = new StreamReader(stream);
        string text = strReader.ReadToEnd();
        items = JsonConvert.DeserializeObject<List<T>>(text);
    }
    catch (WebException)
    {
        throw;
    }
    return items;
}

它会卡在 task.Wait()

我把按钮点击方法改成了async,并在async方法前使用了await,结果为(await GetResponse<string>("url"))。但是Task<List<string>> task = GetResponse<string>("url")有什么问题吗? 我做错了什么?

谢谢帮忙!


10
只是一个小提示,通常约定俗成的是async方法的后缀应该是Async,也就是说你的方法名应该是GetResponseAsync - default
在这里调用 task.Wait(); 是多余的。当需要时,task.Result 已经会调用 task.Wait() - Peter Bruins
4个回答

162
您遇到了典型的死锁问题。task.Wait()task.Result是UI线程中的阻塞调用,这会导致死锁。

不要在UI线程中阻塞。永远都不要这样做。只需使用await即可。

private async void Button_Click(object sender, RoutedEventArgs 
{
      var task = GetResponseAsync<MyObject>("my url");
      var items = await task;
}

顺便问一下,为什么要捕获 WebException 并将其抛回呢?如果你不捕获它会更好。两者是相同的。

此外,我看到你在 GetResponse 方法中混合了异步代码和同步代码。 StreamReader.ReadToEnd 是一个阻塞调用——你应该使用 StreamReader.ReadToEndAsync

还要将返回 Task 或异步的方法使用“Async”后缀,以遵循 TAP(“Task based Asynchronous Pattern”)约定,就像 Jon所说的那样

当你解决了所有上述问题时,你的方法应该看起来像以下内容。

public static async Task<List<T>> GetResponseAsync<T>(string url)
{
    HttpWebRequest request = (HttpWebRequest)HttpWebRequest.Create(url);
    var response = (HttpWebResponse)await Task.Factory.FromAsync<WebResponse>(request.BeginGetResponse, request.EndGetResponse, null);

    Stream stream = response.GetResponseStream();
    StreamReader strReader = new StreamReader(stream);
    string text = await strReader.ReadToEndAsync();

    return JsonConvert.DeserializeObject<List<T>>(text);
}

36

这就是正在致你于死地的东西:

task.Wait();

这会阻塞UI线程,直到任务完成 - 但该任务是一个异步方法,它将在“暂停”并等待异步结果后尝试返回UI线程。由于您正在阻塞UI线程,因此无法执行此操作...

你的代码中没有任何看起来真正需要在UI线程上运行的东西,但假设你确实想要在UI线程上运行,你应该使用:

private async void Button_Click(object sender, RoutedEventArgs 
{
    Task<List<MyObject>> task = GetResponse<MyObject>("my url");
    var items = await task;
    // Presumably use items here
}

或者只需要:

private async void Button_Click(object sender, RoutedEventArgs 
{
    var items = await GetResponse<MyObject>("my url");
    // Presumably use items here
}

现在,Button_Click 方法不再阻塞等待任务完成,而是在安排任务完成时触发一个连续的回调后立即返回(基本上就是 async/await 的工作原理)。

注意,为了清晰起见,我还会将 GetResponse 重命名为 GetResponseAsync


我已经在按钮点击事件处理程序中添加了 async,并且正在使用 await 等待异步任务,但是异步任务仍然在我的主线程上运行,并且阻塞了 Windows Form UI。有任何想法吗,因为我正在遵循以上示例?谢谢。 - Chris Walsh
2
@ChrisWalsh:是的,异步任务仍将在您的主线程上运行。如果它确实是异步的(例如执行异步IO),那应该没问题。如果该任务执行阻塞操作,则可以。我建议您提出一个完整示例的新问题。 - Jon Skeet

0

@ChrisWalsh:如果你使用Task.Run()并在该函数内调用异步任务,该任务将在新的UI线程上运行,防止阻塞你的UI。


-3
请使用以下代码。
 Task.WaitAll(Task.Run(async () => await GetResponse<MyObject>("my url")));

10
Task.WaitAll 会像 task.Wait 一样阻塞代码执行。 - Simon MᶜKenzie
问题是“按钮点击并等待方法完成”,因此上面的代码片段将等待方法完成。 - Mahesh
这正是我所需要的。谢谢。我有一个用户点击一个按钮,该按钮会执行不同的操作,其中之一可能需要调用异步进程。我需要知道如何暂停,让该进程完成,以便我的当前线程可以继续进行下一步。完美地解决了! - da_jokker

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接