async void方法缺乏等待

时间:2015-05-20 00:11:41

标签: c# asynchronous

我第一次尝试进行异步编程,但是它没有像我预期的那样工作。 我有一个加载网址集的按钮(这是从代码片段中省略的)

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会抛出一个错误“这个异步方法缺少等待运算符......”

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源代码中提取所需信息后,将填充Chapters(包含元组的List类型的属性)。我想以异步方式执行此操作,因为对于大量的URL,此过程可能需要一段时间,我不想阻止UI线程(它是一个Windows窗体应用程序)。

我做错了什么?

2 个答案:

答案 0 :(得分:3)

  

但是LoadAllChaptersAsync会抛出错误:

this async method lacks await operators...

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

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

  

我想以异步方式执行此操作,因为对于大量网址,此过程可能需要一段时间,我不想阻止UI线程(它是一个Windows窗体应用程序)。

您可以更改您的方法以返回在后台执行工作的新Task,这将返回包含所有新创建的&#34;章节的新列表&#34;完成任务。如:

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);

其他改进

我们可以对上述解决方案进行两项改进。我们可以为主要受CPU限制且其实现不包括异步/等待的任务合并一些最佳实践。

  1. 让客户端(即调用者)尽可能在调用堆栈中决定是将该方法作为单独的任务调用还是同步调用它。
  2. 如果方法完成的工作可能需要一段时间,您可能希望能够取消它。因此,如果它按步骤或循环执行,请使用与cooperative cancellation相关联的CancellationToken来支持CancellationTokenSource
  3. 对于您的代码,您可能希望提供&#34;停止加载&#34;用户界面中的按钮,单击时,使用以下内容取消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;
    }
    

    可以在此处找到一个非常类似的方法(确实涉及异步操作)以及一些其他解释:"Async Cancellation: Bridging between the .NET Framework and the Windows Runtime"

答案 1 :(得分:1)

当您使用ant jar时,您并没有立即返回代表其工作的async的方法 - 相反,Task方法将返回{{1}当您使用async运算符时,表示其余工作。正如Alex的回答中所提到的,这可以通过Task来完成,但也可以通过await Task.Yield()函数在一个方法中完成,该函数会立即返回。

请注意,在UI应用程序中,您通常会将Task.Run设置为仅使用一个帖子 - 可能需要使用ConfigureAwait来确保您使用其他线程。因此:

await

这只是一种可能性 - 最好通过拨打Thread.CurrentThread并检查ManagedThreadId进行测试,以确保您处于某个特定线程或其他线程上。