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

时间:2015-01-04 12:05:47

标签: c# .net wpf task-parallel-library deadlock

下面的代码导致我的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.stackoverflow.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;
     }
 }

2 个答案:

答案 0 :(得分:1)

挂起的可能原因是你正在混合sync和asnyc代码并调用WaitAny。 Stephen Cleary的帖子对于理解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.stackoverflow.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;
  }
}

答案 1 :(得分:1)

这里最大的问题是你的构造函数在完成所有任务之后才会返回。在构造函数返回之前,窗口将不会显示,因为与绘制窗口相关的窗口消息不会被处理。

请注意,这里并没有真正的“死锁”。相反,如果你等待足够长的时间(即直到所有任务都完成),实际上会显示窗口。

当您将调用添加到MessageBox.Show()时,您可以为UI线程提供处理窗口消息队列的机会。也就是说,普通的模态对话框包括一个线程消息泵,它最终处理队列中的那些消息,包括与显示窗口相关的消息。请注意,即使添加MessageBox.Show(),也不会导致窗口在处理过程中更新。它只允许在再次阻止UI线程之前显示窗口。

解决此问题的一种方法是切换到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()的调用完成时(即任何任务完成),运行时将通过调用UI线程上的continuation来继续执行ProcessURLs()方法。这允许该方法正常访问UI对象(例如this.tbStatusBar.Text),同时占用UI线程的时间仅足以处理完成。

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

其他一些说明:

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

在这个特定的例子中,只要你使用async / await,开始构造函数中的处理可能不会真正伤害任何东西,因为UI相关的东西不是'在任何情况下都能执行,直到至少构造函数返回为止。我只是试图避免在构造函数中做这种事情作为一般规则。

  • 我还修改了你的任务集合的一般处理,以及我认为更合适的东西。它消除了tasks集合的重复重新初始化,并利用了WhenAny()方法的语义。我还删除了AsParallel();由于处理的长时间运行部分已经异步处理,因此尝试并行化Select()本身似乎没有任何优势。