等待所有任务死锁

4

我有一个关于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(与 async/await 一起),就会导致死锁。我的功能可以执行其工作(因此它们被正确执行),但是 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);
            }
        }

    }
}

你可能不应该在异步代码上阻塞。点击这里了解更多信息。如果你将Task.WaitAll改为await Task.WhenAll,会发生什么? - default
1
附注:不要包装同步方法 - default
1
这可能也与以下内容相关:https://dev59.com/Q1kT5IYBdhLWcg3wa-xz - default
如果ReadImages正在进行I/O工作,为什么它不是异步的呢?async和await主要用于I/O工作。 - Camilo Terevinto
1
你还应该添加这个程序运行在哪个框架上(ASP.NET、WinForms、WPF、控制台应用程序...),这样我们就知道使用了什么同步上下文(如果有的话)。 - Camilo Terevinto
显示剩余6条评论
4个回答

7

您正在同步等待任务完成。如果不加一点点 ConfigureAwait(false) 的魔法,这在 WPF 中是行不通的。下面是更好的解决方案:

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的异步版本使其变得非常漂亮和美观:


通过使用 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。当你创建一个任务时,你会复制调度该任务的线程的状态,因此,当你完成await后,可以回到它。如果你在没有Task.Run的情况下调用方法,则表示“我想回到UI线程”。这是不可能的,因为UI线程已经在等待任务,所以它们两个都在等待自己。当你加入另一个任务时,你在说:“UI线程必须安排一个‘外部’任务来安排另一个‘内部’任务,然后我会回来。”


tasks.Add(ReadImagesAsync(tag)); 是的,使用异步方法就可以这样做。即使使用WaitAll也是如此。我不必将Execute设置为异步。 - Khaine
@Khaine,我更新了我的答案,这样你就可以看到如何利用async使它完全异步化。这是编写异步代码“全程”所偏爱的方式。 - FCin
WebClient.DownloadFile 还有一个异步版本,但 DownloadStringAsync 是 void。它说我不能等待它。WebClient.DownloadFileAsync 也是同样的情况。这就是为什么我把它们留在那个状态的原因。另外,tasks.Add(Task.Run(() => ReadImages(tag)));WaitAll 结合起来确实可以工作。只有使用 Task.Run 包装时才会出现问题。 - Khaine
@Khaine 没错,对不起,我没有检查方法声明。是的,添加 Task.Run 并保留 Task.WaitAll 将会起作用,但这并不是编写异步代码的好方法。 - FCin

1
我发现了一些更详细的文章,解释了死锁为什么会发生在这里:

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

好的。就是这样。突然间不再出现死锁了。但是这两篇文章和@FC的回复解释了为什么会发生这种情况。


1
使用WhenAll替代WaitAll,将您的Execute转换为async Task并等待由Task.WhenAll返回的任务。这样可以避免在异步代码上阻塞。

-1

就像你说的,你不在乎ReadImagesAsync()何时完成,但你必须等待它...这里有一个定义

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

Task.WhenAll方法用于创建一个任务,只有当所有其他任务都完成时才会完成。

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

基本上,Task.WhenAll会为您提供一个未完成的任务,但是一旦指定的任务完成执行,您可以使用ContinueWith。请注意,Task.WhenAll方法和Task.WaitAll方法都不会运行任务,即这些方法都不会启动任何任务。

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

  // write your code here

});

在SO中引用外部链接是不被赞同的。如果链接有有用的信息,你可以在这里写出来,否则可以将其留作评论。 - Bizhan

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