Async void方法缺少await

本文关键字:await 方法 void Async | 更新日期: 2023-09-27 17:49:58

我第一次尝试异步编程,但它不像我期望的那样工作。我有一个按钮,加载url的集合(这是从代码片段提交)

private async void btnLoad_Click(object sender, EventArgs e)
{
    foreach (var item in myCollectionOfUrls)
    {
        Uri tempUri = new Uri(item);
        Uri = tempUri; // Uri is a property
        string htmlCode = await LoadHtmlCodeAsync(Uri);
        LoadAllChaptersAsync(htmlCode, Path.GetFileNameWithoutExtension(item));
    }
}

LoadHtmlCodeAsync(Uri)工作正常:

private string LoadHtmlCode(string url)
{
    using (WebClient client = new WebClient())
    {
        try
        {
            System.Threading.Thread.Sleep(0);
            client.Encoding = Encoding.UTF8;
            client.Proxy = null;
            return client.DownloadString(url);
        }
        catch (Exception ex)
        {
            Logger.Log(ex.Message);
            throw;
        }
    }
}

但是LoadAllChaptersAsync抛出错误"这个async方法缺少await操作符…"

private async void LoadAllChaptersAsync(string htmlCode, string mangaName)
{
    HtmlAgilityPack.HtmlDocument htmlDoc = new HtmlAgilityPack.HtmlDocument();
    htmlDoc.LoadHtml(htmlCode);
    var chapterLink = htmlDoc.DocumentNode.SelectNodes(@"//div[@id='chapterlist']//a/@href");
    var chapterName = htmlDoc.DocumentNode.SelectNodes(@"//div[@id='chapterlist']//a/@href/following-sibling::text()[1]").Reverse().ToList();
    for (int i = 0; i < chapterLink.Count; i++)
    {
        var link = "http://" + Uri.Host + chapterLink[i].GetAttributeValue("href", "not found");
        var chName = chapterName[i].OuterHtml.Replace(" : ", "");
        var chapterNumber = chapterLink[i].InnerText;
        Chapters.Add(new Tuple<string, string, string, string>(link, chName, chapterNumber, mangaName));
    }
}

我预期的结果是,章节(类型的属性列表包含一个元组)得到填充后,我完成了提取信息,我需要从html源代码。我想异步地这样做,因为对于大量的url,这个过程可能需要一段时间,我不想阻塞UI线程(它是一个windows窗体应用程序)。

我做错了什么?

Async void方法缺少await

但是LoadAllChaptersAsync抛出错误:

this async method lacks await operators...

这是因为您的LoadAllChaptersAsync方法不执行任何异步操作,await方法也不执行任何异步操作。

一个常见的误解是,在方法中使用async(或await)关键字以某种方式神奇地在不同的线程上创建了一个新任务。

我想异步执行此操作,因为对于大量的url,此过程可能需要一段时间,我不想阻塞UI线程(这是一个windows窗体应用程序)。

你可以改变你的方法来返回一个新的Task,它在后台执行工作,并且它将返回一个包含所有新创建的任务完成"章节"的新列表。如:

private Task<List<Tuple<string, string, string, string>>>
LoadAllChaptersAsync(string htmlCode, string mangaName)
{
    return Task.Run(() {
        var newChapters = new List<Tuple<string, string, string, string>>();
        // ...
        return newChapters;
    });
}

这个任务可以等待,没有必要将你的方法标记为不做任何异步的async

var newChapters = await LoadAllChaptersAsync(htmlCode, Path.GetFileNameWithoutExtension(item));
Chapters.AddRange(newChapters);
<<p> 额外的改进/strong>

对于上面的解决方案,我们可以做两个改进。我们可以结合一些最佳实践的任务,主要是CPU的限制,其实现不包括async/await。

  1. 让客户端(即调用者)在调用堆栈中尽可能高的位置决定是否将该方法作为单独的任务调用或同步调用。
  2. 如果该方法完成的工作可能需要一段时间,您可能希望能够取消它。因此,如果它是按步骤或循环工作的,那么通过使用CancellationToken连接到CancellationTokenSource,使其支持协作取消。

对于您的代码,您可能希望在UI中提供一个"停止加载"按钮,当单击时,使用以下命令来取消在LoadAllChaptersAsync方法中执行的工作:

private async void btnStopLoading_Click(object sender, EventArgs e)
{
    if (_loadChaptersCancelSource != null)
        _loadChaptersCancelSource.Cancel();
}

那么你的原始代码可以改为:

private async void btnLoad_Click(object sender, EventArgs e)
{
    if (_loadChaptersCancelSource == null)
    {
        var wasCancelled = false;
        _loadChaptersCancelSource = new CancellationTokenSource();
        try
        {
            var token = _loadChaptersCancelSource.Token;
            foreach (var item in myCollectionOfUrls)
            {
                // stop if cancellation was requested.
                token.ThrowIfCancellationRequested();
                Uri tempUri = new Uri(item);
                Uri = tempUri; // Uri is a property
                // also modified to be cancellable.
                string htmlCode = await LoadHtmlCodeAsync(Uri, token); 
                // client decides to run as a background task
                var newChapters = await Task.Run(() =>  
                    LoadAllChapters(htmlCode, Path.GetFileNameWithoutExtension(item), token), 
                    token);
                Chapters.AddRange(newChapters);
            }
        }
        catch (OperationCanceledException) 
        { 
            wasCancelled = true;
        }
        catch (AggregateException ex) 
        {
            if (!ex.InnerExceptions.Any(e => typeof(OperationCanceledException).IsAssignableFrom(e.GetType())))
                throw; // not cancelled, different error.
            wasCancelled = true;
        }
        finally
        {
            var cts = _loadChaptersCancelSource;
            _loadChaptersCancelSource = null;
            cts.Dispose();
        }
        if (wasCancelled)
            ; // Show a message ?
    }
}

你的LoadAllChapters可以是一个常规的同步方法,允许协作取消:

private List<Tuple<string, string, string, string>>
LoadAllChapters(string htmlCode, string mangaName, CancellationToken cancelToken)
{
    HtmlAgilityPack.HtmlDocument htmlDoc = new HtmlAgilityPack.HtmlDocument();
    htmlDoc.LoadHtml(htmlCode);
    // Don't continue if cancelation is requested
    cancelToken.ThrowIfCancellationRequested();
    var chapterLink = htmlDoc.DocumentNode.SelectNodes(@"//div[@id='chapterlist']//a/@href");
    var chapterName = htmlDoc.DocumentNode.SelectNodes(@"//div[@id='chapterlist']//a/@href/following-sibling::text()[1]").Reverse().ToList();
    var newChapters = new List<Tuple<string, string, string, string>>();
    for (int i = 0; i < chapterLink.Count; i++)
    {
        // Stop the loop if cancellation is requested.
        cancelToken.ThrowIfCancellationRequested();
        var link = "http://" + Uri.Host + chapterLink[i].GetAttributeValue("href", "not found");
        var chName = chapterName[i].OuterHtml.Replace(" : ", "");
        var chapterNumber = chapterLink[i].InnerText;
        newChapters.Add(new Tuple<string, string, string, string>(link, chName, chapterNumber, mangaName));
    }
    return newChapters;
}

一个非常相似的方法(涉及到异步操作)可以在这里找到一些额外的解释:"异步取消:在。net框架和Windows运行时之间架桥"

当您使用async时,您不会立即返回代表其自己工作的Task的方法-相反,当您使用await操作符时,async方法将返回代表其余工作的Task 。正如Alex的回答中提到的,这可以通过Task.Run完成,但也可以通过await调用Task.Yield()函数在方法中完成,该函数会立即返回。

请注意,在UI应用程序中,您通常会将SynchronizationContext设置为仅使用一个线程-可能有必要使用ConfigureAwait以确保您在另一个线程上。因此:

await Task.Yield().ConfigureAwait(false);

这只是一种可能性,不过-最好通过调用Thread.CurrentThread并检查ManagedThreadId来测试,以确保您在一个特定的线程或另一个。