为什么这会导致应用程序挂起

Why does this causes the application to hang

下面的代码导致我的 WPF 应用程序挂起(可能是死锁)。我已验证 DownloadStringAsTask 方法是在单独的(非 UI)线程上执行的。有趣的是,如果您取消注释消息框行(就在调用 while (tasks.Any() 之前),应用程序工作正常。谁能解释为什么应用程序首先挂起以及如何解决这个问题?

<Window x:Class="WpfApplication1.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="350" Width="525">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="9*" />
            <RowDefinition Height="*" />
        </Grid.RowDefinitions>
        <Frame x:Name="frame"  Grid.Row="0" />
        <StatusBar VerticalAlignment="Bottom"  Grid.Row="1" >
            <StatusBarItem>
                <TextBlock Name="tbStatusBar" Text="Waiting for getting update" />
            </StatusBarItem>
        </StatusBar>
    </Grid>
</Window>



public partial class MainWindow : Window
{
    List<string> URLsToProcess = new List<string>
        {
             "http://www.microsoft.com",
             "http://www.whosebug.com",
             "http://www.google.com",
             "http://www.apple.com",
             "http://www.ebay.com",
             "http://www.oracle.com",
             "http://www.gmail.com",
             "http://www.amazon.com",
             "http://www.outlook.com",
             "http://www.yahoo.com",
             "http://www.amazon124.com",
             "http://www.msn.com"
         };

    public MainWindow()
    {
        InitializeComponent();
        ProcessURLs();
    }

    public void ProcessURLs()
    {
         var tasks = URLsToProcess.AsParallel().Select(uri => DownloadStringAsTask(new Uri(uri))).ToArray();
         //MessageBox.Show("this is doing some magic"); 
         while (tasks.Any())
         {
             try
             {
                 int index = Task.WaitAny(tasks);
                 this.tbStatusBar.Text = string.Format("{0} has completed", tasks[index].AsyncState.ToString());
                 tasks = tasks.Where(t => t != tasks[index]).ToArray();
             }
             catch (Exception e)
             {
                 foreach (var t in tasks.Where(t => t.Status == TaskStatus.Faulted))
                     this.tbStatusBar.Text = string.Format("{0} has completed", t.AsyncState.ToString());
                 tasks = tasks.Where(t => t.Status != TaskStatus.Faulted).ToArray();
             }
         }   
    }

    private Task<string> DownloadStringAsTask(Uri address)
     {
         TaskCompletionSource<string> tcs = new TaskCompletionSource<string>(address);
         WebClient client = new WebClient();
         client.DownloadStringCompleted += (sender, args) =>
         {
             if (args.Error != null)
                 tcs.SetException(args.Error);
             else if (args.Cancelled)
                 tcs.SetCanceled();
             else
                 tcs.SetResult(args.Result);
         };
         client.DownloadStringAsync(address);
         return tcs.Task;
     }
 }

挂起的可能原因是您混合了同步和 asnyc 代码并调用了 WaitAny。 Stephen Cleary 的 post 有助于理解 Tasks 的常见问题。 Best Practices in Asynchronous Programming

这是一个简化代码并使用 Parallel.ForEach

的解决方案

代码

public partial class WaitAnyWindow : Window {

  private List<string> URLsToProcess = new List<string>
      {
            "http://www.microsoft.com",
            "http://www.whosebug.com",
            "http://www.google.com",
            "http://www.apple.com",
            "http://www.ebay.com",
            "http://www.oracle.com",
            "http://www.gmail.com",
            "http://www.amazon.com",
            "http://www.outlook.com",
            "http://www.yahoo.com",
            "http://www.amazon.com",
            "http://www.msn.com"
        };

  public WaitAnyWindow02() {
    InitializeComponent();
    Parallel.ForEach(URLsToProcess, (x) => DownloadStringFromWebsite(x));
  }

  private bool DownloadStringFromWebsite(string website) {
    WebClient client = new WebClient();
    client.DownloadStringCompleted += (s, e) =>
    {
      if (e.Error != null)
      {
        Dispatcher.BeginInvoke((Action)(() =>
        {
          this.tbStatusBar.Text = string.Format("{0} didn't complete because {1}", website, e.Error.Message);
        }));
      }
      else
      {
        Dispatcher.BeginInvoke((Action)(() =>
        {
          this.tbStatusBar.Text = string.Format("{0} has completed", website);
        }));
      }
    };

    client.DownloadStringAsync(new Uri(website));

    return true;
  }
}

这里最大的问题是您的构造函数在所有任务完成之前不会return。在构造函数 return 之前,不会显示 window,因为不会处理与绘制 window 相关的 window 消息。

请注意,您实际上并没有 "deadlock" 本身。相反,如果您等待的时间足够长(即直到所有任务都完成),window 实际上会显示。

当您添加对 MessageBox.Show() 的调用时,您为 UI 线程提供了处理 window 消息队列的机会。也就是说,正常的模态对话框包括一个线程消息泵,它最终处理队列中的那些消息,包括那些与显示你的 window 相关的消息。请注意,即使您添加 MessageBox.Show(),也不会导致 window 随着处理的进行而更新。它只是允许在您再次阻止 UI 线程之前显示 window。

解决此问题的一种方法是切换到 async/await 模式。例如:

public MainWindow()
{
    InitializeComponent();
    var _ = ProcessURLs();
}

public async Task ProcessURLs()
{
    List<Task<string>> tasks = URLsToProcess.Select(uri => DownloadStringAsTask(new Uri(uri))).ToList();

    while (tasks.Count > 0)
    {
        Task<string> task = await Task.WhenAny(tasks);
        string messageText;

        if (task.Status == TaskStatus.RanToCompletion)
        {
            messageText = string.Format("{0} has completed", task.AsyncState);
            // TODO: do something with task.Result, i.e. the actual downloaded text
        }
        else
        {
            messageText = string.Format("{0} has completed with failure: {1}", task.AsyncState, task.Status);
        }

        this.tbStatusBar.Text = messageText;
        tasks.Remove(task);
    }

    tbStatusBar.Text = "All tasks completed";
}

我已将 ProcessURLs() 方法重写为 async 方法。这意味着当构造函数调用它时,它将 运行 同步到第一个 await 语句,此时它将让出并允许当前线程正常继续。

当对 Task.WhenAny() 的调用完成时(即任何任务完成),运行time 将通过调用 [=] 上的延续来恢复执行 ProcessURLs() 方法66=]线程。这允许该方法正常访问 UI 对象(例如 this.tbStatusBar.Text),同时只占用 UI 线程足够长的时间来处理完成。

当循环 return 到达顶部并再次调用 Task.WhenAny() 方法时,将重复整个序列(即循环应该工作的方式 :))。

一些其他注意事项:

  • 构造函数中的 var _ = 位用于抑制在忽略 Task return 值时出现的编译器警告。
  • 恕我直言,最好不要在构造函数中初始化这些操作。构造函数通常不是进行此类重要工作的好地方。相反,我会(例如)覆盖 OnActivated() 方法,使其成为 async 这样你就可以使用 await 语句调用 ProcessURLs() (即更惯用的方式调用 async 方法)。这确保 window 在您开始进行任何其他处理之前完全初始化并显示。

在此特定示例中,只要您使用 async/await,在构造函数中开始处理可能不会真正造成任何伤害,因为 UI 相关的东西在任何情况下都无法执行,直到至少构造函数有 returned。作为一般规则,我只是尽量避免在构造函数中做这种事情。

  • 我还修改了您的任务集合的一般处理方式,使其更适合我。它摆脱了 tasks 集合的重复重新初始化,并利用了 WhenAny() 方法的语义。我还删除了 AsParallel();鉴于处理的 long-运行ning 部分已经异步处理,尝试并行化 Select() 本身似乎没有任何优势。