我不理解await/async如何工作。

3
我开始研究c#中Task、async/await的概念,但是我很难理解它们,至少我不知道如何实现它们。我开始重写之前编写的一个测试程序,但现在我想使用这些新概念,而不是线程。基本上布局如下:
我有一个简单的类,用来下载网页的HTML内容。我在另一个类中处理它,基本上只是将页面解析为我的模型。稍后我想在我的UI上显示它。问题是,我的程序没有响应,当我处理信息时会阻塞UI。
两天前我开始学习这个,我已经阅读了很多在线资料,包括MSDN和一些博客,但我仍然无法弄清楚它。也许有人也能提供帮助。
HtmlDownloadCode:
public async Task<string> GetMangaDescriptionPage(string detailUrl)
{
    WebClient client = new WebClient();

    Stream data = await client.OpenReadTaskAsync(detailUrl);
    StreamReader reader = new StreamReader(data);
    string s = reader.ReadToEnd();
    data.Dispose();
    reader.Dispose();
    data.Close();
    reader.Close();
    return s;     
}

我的解析类代码:

public async  Task<MangaDetailsModel> ParseMangaDescriptionPage()
{
    ParseOneManga pom = new ParseOneManga();
    string t1 = await pom.GetMangaDescriptionPage(selectedManga.url);
    HtmlDocument htmlDoc = new HtmlDocument();
        htmlDoc.LoadHtml(t1);
        var divs = htmlDoc.DocumentNode.Descendants("div").Where(x => x.Attributes.Contains("id") &&
            x.Attributes["id"].Value.Contains("title")).ToArray();
        mangaDetails.mangaName = divs[0].Element("h1").InnerText;

        mangaDetails.description = divs[0].Descendants("p").Single().InnerText ?? "DSA";
        var tds = divs[0].Descendants("td");
        int info = 0;

    var chapters = htmlDoc.DocumentNode.Descendants("div").Where(x => x.Attributes.Contains("id") &&
        x.Attributes["id"].Value.Contains("chapters")).ToArray();
    var chapterUi = chapters[0].Descendants("ul").Where(x => x.Attributes.Contains("class") &&
    x.Attributes["class"].Value.Contains("chlist"));
    foreach (var li in chapterUi)
    {
        var liChapter = li.Descendants("li");
        foreach (var h3tag in liChapter)
        {
            var chapterH3 = h3tag.Descendants("a").ToArray();
            SingleManagFox chapterData = new SingleManagFox();
            chapterData.name = chapterH3[1].InnerHtml;
            chapterData.url = chapterH3[1].GetAttributeValue("href", "0");
            mangaDetails.chapters.Add(chapterData);
        }
    };

    return mangaDetails;
}

UI代码:

private async   void mainBtn_Click(object sender, RoutedEventArgs e)
{
    if (mangaList.SelectedItem != null)
    {
         test12((SingleManagFox)mangaList.SelectedItem);     
    }
}

private async void test12(SingleManagFox selectedManga)
{
    selectedManga = (SingleManagFox)mangaList.SelectedItem;
    MangaDetails mangaDetails = new MangaDetails(selectedManga);
    MangaDetailsModel mdm = await mangaDetails.ParseMangaDescriptionPage();
    txtMangaArtist.Text = mdm.artisName;
    txtMangaAuthor.Text = mdm.authorName;
    chapterList.ItemsSource = mdm.chapters;
}    

对不起,如果这个问题太简单了,但我自己无法解决。


2
string s = reader.ReadToEnd(); -> string s = await reader.ReadToEndAsync(); - user4003407
1
一个初始的观察。除非它是事件处理程序,否则请避免使用async voidtest12应该更新为返回Task并在事件处理程序mainBtn_Click中等待。当进行异步操作时,您需要尽可能地全程异步。避免阻塞调用。 - Nkosi
@PetSerAl谢谢,问题已经解决了。我猜测是用户界面线程卡住了,在此再次感谢您。 - Ladybird
2个回答

1
当进行异步操作时,您需要尽量避免混合阻塞调用和异步调用。
您在事件处理程序中使用了没有await的async void。
除非是事件处理程序,否则请避免使用async void。test12应该更新为返回Task并在事件处理程序mainBtn_Click中等待。
private async void mainBtn_Click(object sender, RoutedEventArgs e) {
    if (mangaList.SelectedItem != null) {
       await test12((SingleManagFox)mangaList.SelectedItem);
    }
}

private async Task test12(SingleManagFox selectedManga) {
    selectedManga = (SingleManagFox)mangaList.SelectedItem;
    MangaDetails mangaDetails = new MangaDetails(selectedManga);
    MangaDetailsModel mdm = await mangaDetails.ParseMangaDescriptionPage();
    txtMangaArtist.Text = mdm.artisName;
    txtMangaAuthor.Text = mdm.authorName;
    chapterList.ItemsSource = mdm.chapters;
}  

如果可用的话,还要考虑更新 Web 调用以使用 HttpClient

class ParseOneManga {
    public async Task<string> GetMangaDescriptionPageAsync(string detailUrl) {
        using (var client = new HttpClient()) {
            string s = await client.GetStringAsync(detailUrl);
            return s;                
        }
    }
}

参考文献:- 异步/等待 - 异步编程的最佳实践


谢谢你的帮助,但我猜核心问题在于PetSerAl读取器阻塞了它。感谢提示,现在两种情况都可以工作了,但标准是即使发生事件(我点击按钮,所以我认为在我的函数中将其设置为void是可以的),也要返回一个任务。如果你有时间,还有一个“离题”的问题,我两天前开始使用task/async,但我觉得我进展得很慢,我有一种编程不是我的强项的感觉,好像其他人更快地掌握了这些东西。我进展得太慢了吗? - Ladybird
我仍然无法为您的问题授予积分,但当我获得所需的声望时,我会这样做。 - Ladybird
如果您还没有阅读过,请查看此文章。https://msdn.microsoft.com/zh-cn/magazine/jj991977.aspx - Nkosi
@user3398990,针对您上述问题的一般解决方案是避免混合使用阻塞调用和异步调用。调用sream.ReadToEnd是一个IO阻塞调用。 - Nkosi
是的,我已经检查过了,理解这些概念很“简单”,但当我开始工作时,情况有些不同...我想我缺乏经验或者对这些东西太蠢了。这也是我第一次使用这些东西,所以我不知道。 - Ladybird
@user3398990 这是一个学习曲线。慢慢来,你会逐渐掌握它的。如果你试图匆忙完成事情,你只会让自己更加困惑。 - Nkosi

0

很多人认为 async-await 意味着多个线程同时处理您的代码。除非您显式地启动不同的线程,否则情况并非如此。

帮助我解释 async-await 的好比喻是 Eric Lippert 在 this interview with Eric Lippert 中使用的餐厅比喻。在文章中间某处搜索 async-await。

Eric Lippert 将 async-await 处理与一个需要等待水开的厨师进行了比较。他在等待时四处看看是否可以做其他事情。完成其他事情后,他回来查看水是否开了,并开始加工开水。

您的进程也是如此。只有一个线程繁忙(一次)。该线程将继续处理,直到必须等待某些内容。这些内容通常是一个相当长的过程,该过程在不使用 CPU 核心的情况下进行处理,例如将文件写入磁盘、加载网页或查询外部数据库中的信息。

你的线程一次只能做一件事情。所以当它在忙于计算某些东西时,如果不能对操作员输入做出反应,你的UI就会冻结,直到计算完成。异步等待只有在你的线程需要等待其他进程完成很多次时才有帮助。

如果你调用一个异步函数,你可以确定在那个函数中的某个地方有一个等待。实际上,如果你声明了一个异步函数,而你忘记在其中等待,你的编译器会警告你。

当你的调用遇到函数中的等待时,你的线程会沿着调用堆栈向上走,看看它是否可以做其他事情。如果你没有等待,你可以继续处理,直到你必须等待。线程再次沿着调用堆栈向上走,看看是否有一个调用者没有等待等等。

async Task ReadDataAsync() { // 做一些准备工作 using (TextReader textReader = ...) { var myReadTask = textReader.ReadToEndAsync(); // 当textReader正在等待信息可用时 // 你可以做其他的事情 ProcessSomething();

        // after a while you really need the results from the read data,
        // so you await for it.
        string text = await MyReadTask;
        // after the await, the results from ReatToEnd are available
        Process(text);
        ...

编程中有一些规则需要遵循:

  • 异步函数应该返回Task而不是void,以及Task<TResult>而不是TResult
  • 有一个例外:异步事件处理程序返回void而不是Task
  • 在异步函数内部,应该使用await等待。如果不等待,则声明函数为异步没有意义
  • await Task的结果是void,await Task<TResult>的结果是TResult
  • 如果调用异步函数,请尝试进行一些处理,而不是等待调用结果

请注意,即使在等待多个异步函数之前调用了几个异步函数,也不意味着多个线程正在同步运行这些函数。在第一次调用异步函数后,其后的语句会在被调用的函数开始等待后处理。

async Task DoSomethingAsync()
{
    var task1 = ReadAsync(...);
    // no await, so next statement processes as soon as ReadAsync starts awaiting
    DoSomeThingElse();

    var task2 = QueryAsync(...);
    // again no await

    // now I need results from bothtask1, or from task2:
    await Task.WhenAll(new Task[] {task1, task2});

    var result1 = Task1.Result;
    var result2 = Task2.Result;
    Process(result1, result2);
    ...

通常,所有异步功能都由同一上下文执行。实际上,这意味着您可以编写单线程程序。这使得程序的外观更加简单。
另一篇对我帮助很大的文章是Async-Await best practices written by the ever so helpful Stephen Cleary。它帮助我更好地理解了异步等待。

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