使用 async/await 和 Task.Run 时未显示特定异常

Specific exceptions are not shown when using async/await and Task.Run

我有一个包含食谱列表的网页。在我的代码中,我循环浏览页面并下载每个食谱。这是我正在做的伪代码:

//This is the Recipe class
//The constructor accepts a link to the recipe
//This method scrapes the recipe
public Task ScrapeAsync(){
    return Task.Run(async () => {
        string WebPage;
        WebRequest request = WebRequest.Create(link);
        request.Method = "GET";
        using (WebResponse response = await request.GetResponseAsync())
        using (StreamReader reader = new StreamReader(response.GetResponseStream())) {
            WebPage = await reader.ReadToEndAsync();
        }
        //Non - async code here which is used to scrape the webpage
    }
}

我使用了Task.Run,因为方法中既有异步代码也有阻塞代码。

//This is the RecipePage class
//This method scrapes the page
public Task GetRecipeListAsync(){
    return Task.Run(async () => {
        //I get the page using WebRequest and WebResponse in the same way as above

        //Non - async/blocking code here which scrapes the page and finds links to recipes. I do await Recipe.ScrapeAsync() somewhere here.
        //And I add the recipe objects to a public list in this class
    }
}

在表单中,它遍历页面列表并执行 await RecipePage.GetRecipeList() 和其他操作。
这是 for 循环所在的位置:

private async void go_Click(object sender, EventArgs e){
    for (int i = (int)startingPageNUD.Value; i <= (int)finishingPageNUD.Value; ++i) {
        RecipePage page = new RecipePage(page + i);
        await page.GetRecipeListAsync();
        //Some other code here
    }
}

我的问题是,每当 ScrapeAsync 方法发生异常时,Visual Studio 2013 指向 Application.Run(new Form1())

static void Main()
    {
        Application.EnableVisualStyles();
        Application.SetCompatibleTextRenderingDefault(false);
        Application.Run(new Form1());
    }

并告诉我 Reflection.TargetInvocationException 已经发生。它没有显示我的代码中的实际异常。例如,如果我在代码中得到 NullReferenceException,它不会显示。 因此,我不得不同时编写异步和非异步代码,并使用非异步代码进行调试。有什么办法可以解决这个问题吗?
我还有一个问题。我是否以正确的方式使用 async/await 和任务?

将getRecipeList代码封装在一个try/catch中。

在捕获代码中,获取异常消息和堆栈跟踪并将它们分配给食谱 class 的变量,您将在操作完成后在主线程中 test/display 变量。

private string TheException = "" ;

public Task GetRecipeListAsync()
{
  TheException = "" ;
  Task result = Task.Run(async () => 
  {
    try 
    {  
      //I get the page using WebRequest and WebResponse in the same way as above
      ...
     }
     catch (Exception Ex)  
  }
  if (TheException!="") MessageBox.show(TheException) ; 
  // or Throw an exception with
  return result ; 
}

您也可以在 GoClick 程序中测试 "TheExceptio"。

好的。感谢您的编辑,我现在可以回答了。

你的问题是这个函数:

private async void go_Click(object sender, EventArgs e){
    for (int i = (int)startingPageNUD.Value; i <= (int)finishingPageNUD.Value; ++i) {
        RecipePage page = new RecipePage(page + i);
        await page.GetRecipeListAsync();
        //Some other code here
    }
}

问题是函数 returns 到达 await 后调用它的对象。现在,如果 page.GetRecipeListAsync(); 抛出异常,则该异常会在延续处理程序中抛出。此处理程序在您的 UI 的任务队列中执行。那里抛出的异常会使 UI 的任务循环崩溃,这会产生各种有趣的效果,包括奇怪的异常。

async void 函数中,您应该 始终 通过将所有代码包装到 trycatch 中来处理任何传入的异常。如果在那里发生异常或不发生异常,您是否使应用程序崩溃由您决定。

事情的一般工作方式是 Taskasync 函数中的扩展抛出的任何异常都会被 await 再次抛出。但是 async void 函数没有在任何地方 awaited,所以那是行不通的。

关于 async 的用法。我认为您不需要将每个函数都包装到 Task 中,但您可以这样做。通常不需要像这样强行进入后台。

Am I using async/await and Task in the correct way?

非常接近。我认为在这种情况下不需要 Task.Run(它应该只用于将 CPU 密集型操作移出 UI 线程,并且 HTML 抓取通常是足够快以保持在 UI 上而没有任何不利影响)。

我要提出的另一个建议是尽可能多地使用 async Task 方法 return 值,而不是修改成员变量作为副作用.在您的情况下,考虑使用 ScrapeAsync return 自己的列表而不是更新共享列表,并让调用代码进行列表合并。

关于您的例外情况,TargetInvocationException 将有一个 InnerException 包含详细信息。 async void 事件处理程序中的异常实际上与常规 void 事件处理程序中的异常管理方式相同:如果一个事件处理程序转义,那么它会进入应用程序主循环。如果你不想要这个,你必须抓住它(对于同步和异步处理程序)。