为什么这会导致应用程序挂起
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()
本身似乎没有任何优势。
下面的代码导致我的 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()
本身似乎没有任何优势。