Task.WaitAll死锁

时间:2018-08-11 16:05:04

标签: c# wpf asynchronous async-await

我对Task.WaitAll有疑问。最初,我尝试使用async / await来获得如下信息:

private async Task ReadImagesAsync(string HTMLtag)
{
    await Task.Run(() =>
    {
        ReadImages(HTMLtag);
    });
}

此功能的内容无关紧要,它可以同步工作并且完全独立于外界。

我这样使用它:

private void Execute()
{
    string tags = ConfigurationManager.AppSettings["HTMLTags"];

    var cursor = Mouse.OverrideCursor;
    Mouse.OverrideCursor = System.Windows.Input.Cursors.Wait;
    List<Task> tasks = new List<Task>();
    foreach (string tag in tags.Split(';'))
    {
         tasks.Add(ReadImagesAsync(tag));
         //tasks.Add(Task.Run(() => ReadImages(tag)));
    }

    Task.WaitAll(tasks.ToArray());
    Mouse.OverrideCursor = cursor;
}

不幸的是,如果我以这种方式(通过异步/等待)使用Task.WaitAll,则会陷入僵局。我的函数完成了自己的工作(因此它们可以正确执行),但是Task.WaitAll永远呆在这里,因为显然ReadImagesAsync不会返回给调用者。

注释行是实际上正确运行的方法。如果我在tasks.Add(ReadImagesAsync(tag));行中注释并使用tasks.Add(Task.Run(() => ReadImages(tag)));-一切正常。

我在这里想念什么?

ReadImages方法看起来像这样:

private void ReadImages (string HTMLtag)
{
    string section = HTMLtag.Split(':')[0];
    string tag = HTMLtag.Split(':')[1];

    List<string> UsedAdresses = new List<string>();
    var webClient = new WebClient();
    string page = webClient.DownloadString(Link);

    var siteParsed = Link.Split('/');

    string site = $"{siteParsed[0]} + // + {siteParsed[1]} + {siteParsed[2]}";

    int.TryParse(MinHeight, out int minHeight);
    int.TryParse(MinWidth, out int minWidth);

    int index = 0;

    while (index < page.Length)
    {
        int startSection = page.IndexOf("<" + section, index);
        if (startSection < 0)
            break;

        int endSection = page.IndexOf(">", startSection) + 1;
        index = endSection;

        string imgSection = page.Substring(startSection, endSection - startSection);

        int imgLinkStart = imgSection.IndexOf(tag + "=\"") + tag.Length + 2;
        if (imgLinkStart < 0 || imgLinkStart > imgSection.Length)
            continue;

        int imgLinkEnd = imgSection.IndexOf("\"", imgLinkStart);
        if (imgLinkEnd < 0)
            continue;

        string imgAdress = imgSection.Substring(imgLinkStart, imgLinkEnd - imgLinkStart);

        string format = null;
        foreach (var imgFormat in ConfigurationManager.AppSettings["ImgFormats"].Split(';'))
        {
            if (imgAdress.IndexOf(imgFormat) > 0)
            {
                format = imgFormat;
                break;
            }
        }

        // not an image
        if (format == null)
            continue;

        // some internal resource, but we can try to get it anyways
        if (!imgAdress.StartsWith("http"))
            imgAdress = site + imgAdress;

        string imgName = imgAdress.Split('/').Last();

        if (!UsedAdresses.Contains(imgAdress))
        {
            try
            {
                Bitmap pic = new Bitmap(webClient.OpenRead(imgAdress));
                if (pic.Width > minHeight && pic.Height > minWidth)
                    webClient.DownloadFile(imgAdress, SaveAdress + "\\" + imgName);
            }
            catch { }
            finally
            {
                UsedAdresses.Add(imgAdress);
            }
        }

    }
}

4 个答案:

答案 0 :(得分:7)

您正在同步等待任务完成。没有一点WPF的魔法,这对ConfigureAwait(false)来说是行不通的。这是一个更好的解决方案:

private async Task Execute()
{
    string tags = ConfigurationManager.AppSettings["HTMLTags"];

    var cursor = Mouse.OverrideCursor;
    Mouse.OverrideCursor = System.Windows.Input.Cursors.Wait;
    List<Task> tasks = new List<Task>();
    foreach (string tag in tags.Split(';'))
    {
         tasks.Add(ReadImagesAsync(tag));
         //tasks.Add(Task.Run(() => ReadImages(tag)));
    }

    await Task.WhenAll(tasks.ToArray());
    Mouse.OverrideCursor = cursor;
}

如果这是WPF,那么我确定您会在发生某种事件时调用它。您应该从事件处理程序中调用此方法,例如:

private async void OnWindowOpened(object sender, EventArgs args)
{
    await Execute();
}

看着问题的编辑版本,我发现实际上您可以通过使用DownloadStringAsync的异步版本来使它们变得非常漂亮:

private async Task ReadImages (string HTMLtag)
{
    string section = HTMLtag.Split(':')[0];
    string tag = HTMLtag.Split(':')[1];

    List<string> UsedAdresses = new List<string>();
    var webClient = new WebClient();
    string page = await webClient.DownloadStringAsync(Link);

    //...
}

现在,tasks.Add(Task.Run(() => ReadImages(tag)));怎么了?

这需要了解SynchronizationContext。创建任务时,您将复制安排任务的线程状态,以便在等待完成后可以返回到该状态。当您调用不带Task.Run的方法时,您会说“我想回到UI线程”。这是不可能的,因为UI线程已经在等待任务,因此它们都在等待自己。当您将其他任务添加到组合中时,您说的是:“ UI线程必须安排一个'外部'任务,该任务将安排另一个我将要返回的'内部'任务。”

答案 1 :(得分:1)

使用WhenAll代替WaitAll,将Execute变成async Task,然后await Task.WhenAll返回的任务。

这样,它永远不会阻塞异步代码。

答案 2 :(得分:0)

我找到了一些更详细的文章来解释为什么实际上在这里发生死锁:

https://medium.com/bynder-tech/c-why-you-should-use-configureawait-false-in-your-library-code-d7837dce3d7f

https://blog.stephencleary.com/2012/07/dont-block-on-async-code.html

简短的回答是对我的异步方法进行了小的更改,因此看起来像这样:

private async Task ReadImagesAsync(string HTMLtag)
{
    await Task.Run(() =>
    {
        ReadImages(HTMLtag);
    }).ConfigureAwait(false);
}

是的。而已。突然之间,它不会死锁。但是这两篇文章+ @FCin响应解释了为什么它实际上发生了。

答案 3 :(得分:-1)

这就像您是说我不在乎ReadImagesAsync()何时完成,但是您必须等待....这是definition

  

Task.WaitAll会阻塞当前线程,直到所有其他任务完成执行为止。

     

Task.WhenAll方法用于创建一个任务,该任务将在且仅当所有其他任务均已完成时才完成。

     

因此,如果您使用的是Task.WhenAll,则会得到一个不完整的任务对象。但是,它不会阻塞并且将允许程序执行。相反,Task.WaitAll方法调用实际上会阻塞并等待所有其他任务完成。

     

本质上,Task.WhenAll将为您提供尚未完成的任务,但是您可以在指定任务完成执行后立即使用ContinueWith。请注意,Task.WhenAll方法和Task.WaitAll都不会运行任务,即,任何一种方法都不会启动任何任务。

Task.WhenAll(taskList).ContinueWith(t => {

  // write your code here

});