簡體   English   中英

異步無效方法缺少等待

[英]async void method lacks await

我正在嘗試第一次進行異步編程,但是它沒有按我期望的那樣工作。 我有一個按鈕可以加載一組網址(此代碼段中省略了)

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(包含Tuple的List類型的屬性)。 我想異步執行此操作,因為對於大量的url,此過程可能需要一段時間,並且我不想阻塞UI線程(這是Windows窗體應用程序)。

我做錯了什么?

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

其他改進

我們可以對上述解決方案進行兩項改進。 對於主要受CPU限制且其實現不包括異步/喚醒的任務,我們可以結合一些最佳實踐。

  1. 讓客戶端(即調用方)在調用堆棧中盡可能高的位置決定是將方法作為單獨的任務調用還是將其同步調用。
  2. 如果使用該方法完成的工作可能需要一段時間,則您可能希望將其取消。 因此,如果它分步或循環地工作,請使用連接到CancellationTokenSourceCancellationToken使其支持協作 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 Framework和Windows運行時之間的橋接”

當您使用async ,您並沒有創建一個立即返回代表其自己工作的Task的方法-相反,當您使用await運算符時, async方法將返回代表其其余工作Task 如Alex的回答所述,這可以通過Task.Run完成,但是也可以通過await Task.Yield()函數從方法內部完成,該函數立即返回。

請注意,在UI應用程序中,通常會將SynchronizationContext設置為僅使用一個線程-可能需要使用ConfigureAwait來確保您位於另一個線程上。 從而:

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

不過,這只是一種可能-最好通過調用Thread.CurrentThread並檢查ManagedThreadId來進行測試,以確保您使用的是某個特定線程或另一個線程。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM