WPF 应用程序中的调度程序实现多个异步任务
Dispatcher in WPF apps implementing multiple async Tasks
在下面的 WPF
应用程序的 MSDN 示例中,它演示了 async/await
多重异步 Tasks
的实现,Dispatcher
对象显然不是 used/needed,即异步执行的 Tasks
似乎可以直接访问 UI 控件(在本例中为 resultTextBox
TextBox
控件 - 请参阅行 resultsTextBox.Text += String.Format("\r\nLength of the download: {0}", length);
)。该应用程序已经过测试,性能符合预期。
但是,问题仍然存在如果此实现能够正确处理可能的竞争条件,例如,如果等待并完成 Task
尝试访问那个 TextBox
控件,而后者仍在处理来自先前完成的 Task
的更新?在实际意义上, 是 WPF Dispatcher
对象仍然需要在 async/await
多任务实现 中处理这种潜在的 concurrency/race 条件问题(或者,可能是互锁功能已经以某种方式隐式地实现在这样的 async/await 编程结构中)?
列表 1。 MSDN 文章 "Start Multiple Async Tasks and Process Them As They Complete" (https://msdn.microsoft.com/en-us/library/jj155756.aspx)
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
// Add a using directive and a reference for System.Net.Http.
using System.Net.Http;
// Add the following using directive.
using System.Threading;
namespace ProcessTasksAsTheyFinish
{
public partial class MainWindow : Window
{
// Declare a System.Threading.CancellationTokenSource.
CancellationTokenSource cts;
public MainWindow()
{
InitializeComponent();
}
private async void startButton_Click(object sender, RoutedEventArgs e)
{
resultsTextBox.Clear();
// Instantiate the CancellationTokenSource.
cts = new CancellationTokenSource();
try
{
await AccessTheWebAsync(cts.Token);
resultsTextBox.Text += "\r\nDownloads complete.";
}
catch (OperationCanceledException)
{
resultsTextBox.Text += "\r\nDownloads canceled.\r\n";
}
catch (Exception)
{
resultsTextBox.Text += "\r\nDownloads failed.\r\n";
}
cts = null;
}
private void cancelButton_Click(object sender, RoutedEventArgs e)
{
if (cts != null)
{
cts.Cancel();
}
}
async Task AccessTheWebAsync(CancellationToken ct)
{
HttpClient client = new HttpClient();
// Make a list of web addresses.
List<string> urlList = SetUpURLList();
// ***Create a query that, when executed, returns a collection of tasks.
IEnumerable<Task<int>> downloadTasksQuery =
from url in urlList select ProcessURL(url, client, ct);
// ***Use ToList to execute the query and start the tasks.
List<Task<int>> downloadTasks = downloadTasksQuery.ToList();
// ***Add a loop to process the tasks one at a time until none remain.
while (downloadTasks.Count > 0)
{
// Identify the first task that completes.
Task<int> firstFinishedTask = await Task.WhenAny(downloadTasks);
// ***Remove the selected task from the list so that you don't
// process it more than once.
downloadTasks.Remove(firstFinishedTask);
// Await the completed task.
int length = await firstFinishedTask;
resultsTextBox.Text += String.Format("\r\nLength of the download: {0}", length);
}
}
private List<string> SetUpURLList()
{
List<string> urls = new List<string>
{
"http://msdn.microsoft.com",
"http://msdn.microsoft.com/library/windows/apps/br211380.aspx",
"http://msdn.microsoft.com/en-us/library/hh290136.aspx",
"http://msdn.microsoft.com/en-us/library/dd470362.aspx",
"http://msdn.microsoft.com/en-us/library/aa578028.aspx",
"http://msdn.microsoft.com/en-us/library/ms404677.aspx",
"http://msdn.microsoft.com/en-us/library/ff730837.aspx"
};
return urls;
}
async Task<int> ProcessURL(string url, HttpClient client, CancellationToken ct)
{
// GetAsync returns a Task<HttpResponseMessage>.
HttpResponseMessage response = await client.GetAsync(url, ct);
// Retrieve the website contents from the HttpResponseMessage.
byte[] urlContents = await response.Content.ReadAsByteArrayAsync();
return urlContents.Length;
}
}
}
注意:我要感谢 Stephen Cleary 的出色回答,而不是有见地的解释,并且还想强调他的解决方案中概述的建议改进,即:用封装在一行代码中的相当紧凑的解决方案替换原始 MSDN 示例中利用 WhenAny
的 unnecessary/complex 代码块,即: await Task.WhenAll(downloadTasks);
(顺便说一句,我在很多实用的应用程序中都使用了这个替代方案,特别是处理 w/multiple 股票网络查询的在线市场数据应用程序)。
非常感谢,斯蒂芬!
However, the question still remains if this implementation is capable of proper handling the possible race condition, for e.g., if the awaited and completed Task tries to access that TextBox control while the latter is still processing the update from previously completed Task?
没有竞争条件。 UI 线程一次只做一件事。
In practical sense, is WPF Dispatcher object still required to handle this potential concurrency/race condition issues in async/await multitasking implementation (or, may be, the interlocking functionality has been somehow implicitly implemented in such async/await programming construct)?
是的,但您不必明确使用它。正如我在 async
intro 中所描述的,await
关键字(默认情况下)将捕获当前上下文并在该上下文中继续执行 async
方法。 "context" 是 SynchronizationContext.Current
(如果当前 SyncCtx 是 null
,则 TaskScheduler.Current
)。
在这种情况下,它将捕获一个 UI SynchronizationContext
,它在幕后使用 WPF Dispatcher 来调度 [=33= 上的 async
方法的其余部分] 线程。
附带说明一下,我不太喜欢“Task.WhenAny
列表并在完成时从列表中删除”方法。我发现如果您通过添加 DownloadAndUpdateAsync
方法进行重构,代码会更清晰:
async Task AccessTheWebAsync(CancellationToken ct)
{
HttpClient client = new HttpClient();
// Make a list of web addresses.
List<string> urlList = SetUpURLList();
// ***Create a query that, when executed, returns a collection of tasks.
IEnumerable<Task> downloadTasksQuery =
from url in urlList select DownloadAndUpdateAsync(url, client, ct);
// ***Use ToList to execute the query and start the tasks.
List<Task> downloadTasks = downloadTasksQuery.ToList();
await Task.WhenAll(downloadTasks);
}
async Task DownloadAndUpdateAsync(string url, HttpClient client, CancellationToken ct)
{
var length = await ProcessURLAsync(url, client, ct);
resultsTextBox.Text += String.Format("\r\nLength of the download: {0}", length);
}
async Task<int> ProcessURLAsync(string url, HttpClient client, CancellationToken ct)
{
// GetAsync returns a Task<HttpResponseMessage>.
HttpResponseMessage response = await client.GetAsync(url, ct);
// Retrieve the website contents from the HttpResponseMessage.
byte[] urlContents = await response.Content.ReadAsByteArrayAsync();
return urlContents.Length;
}
在下面的 WPF
应用程序的 MSDN 示例中,它演示了 async/await
多重异步 Tasks
的实现,Dispatcher
对象显然不是 used/needed,即异步执行的 Tasks
似乎可以直接访问 UI 控件(在本例中为 resultTextBox
TextBox
控件 - 请参阅行 resultsTextBox.Text += String.Format("\r\nLength of the download: {0}", length);
)。该应用程序已经过测试,性能符合预期。
但是,问题仍然存在如果此实现能够正确处理可能的竞争条件,例如,如果等待并完成 Task
尝试访问那个 TextBox
控件,而后者仍在处理来自先前完成的 Task
的更新?在实际意义上, 是 WPF Dispatcher
对象仍然需要在 async/await
多任务实现 中处理这种潜在的 concurrency/race 条件问题(或者,可能是互锁功能已经以某种方式隐式地实现在这样的 async/await 编程结构中)?
列表 1。 MSDN 文章 "Start Multiple Async Tasks and Process Them As They Complete" (https://msdn.microsoft.com/en-us/library/jj155756.aspx)
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
// Add a using directive and a reference for System.Net.Http.
using System.Net.Http;
// Add the following using directive.
using System.Threading;
namespace ProcessTasksAsTheyFinish
{
public partial class MainWindow : Window
{
// Declare a System.Threading.CancellationTokenSource.
CancellationTokenSource cts;
public MainWindow()
{
InitializeComponent();
}
private async void startButton_Click(object sender, RoutedEventArgs e)
{
resultsTextBox.Clear();
// Instantiate the CancellationTokenSource.
cts = new CancellationTokenSource();
try
{
await AccessTheWebAsync(cts.Token);
resultsTextBox.Text += "\r\nDownloads complete.";
}
catch (OperationCanceledException)
{
resultsTextBox.Text += "\r\nDownloads canceled.\r\n";
}
catch (Exception)
{
resultsTextBox.Text += "\r\nDownloads failed.\r\n";
}
cts = null;
}
private void cancelButton_Click(object sender, RoutedEventArgs e)
{
if (cts != null)
{
cts.Cancel();
}
}
async Task AccessTheWebAsync(CancellationToken ct)
{
HttpClient client = new HttpClient();
// Make a list of web addresses.
List<string> urlList = SetUpURLList();
// ***Create a query that, when executed, returns a collection of tasks.
IEnumerable<Task<int>> downloadTasksQuery =
from url in urlList select ProcessURL(url, client, ct);
// ***Use ToList to execute the query and start the tasks.
List<Task<int>> downloadTasks = downloadTasksQuery.ToList();
// ***Add a loop to process the tasks one at a time until none remain.
while (downloadTasks.Count > 0)
{
// Identify the first task that completes.
Task<int> firstFinishedTask = await Task.WhenAny(downloadTasks);
// ***Remove the selected task from the list so that you don't
// process it more than once.
downloadTasks.Remove(firstFinishedTask);
// Await the completed task.
int length = await firstFinishedTask;
resultsTextBox.Text += String.Format("\r\nLength of the download: {0}", length);
}
}
private List<string> SetUpURLList()
{
List<string> urls = new List<string>
{
"http://msdn.microsoft.com",
"http://msdn.microsoft.com/library/windows/apps/br211380.aspx",
"http://msdn.microsoft.com/en-us/library/hh290136.aspx",
"http://msdn.microsoft.com/en-us/library/dd470362.aspx",
"http://msdn.microsoft.com/en-us/library/aa578028.aspx",
"http://msdn.microsoft.com/en-us/library/ms404677.aspx",
"http://msdn.microsoft.com/en-us/library/ff730837.aspx"
};
return urls;
}
async Task<int> ProcessURL(string url, HttpClient client, CancellationToken ct)
{
// GetAsync returns a Task<HttpResponseMessage>.
HttpResponseMessage response = await client.GetAsync(url, ct);
// Retrieve the website contents from the HttpResponseMessage.
byte[] urlContents = await response.Content.ReadAsByteArrayAsync();
return urlContents.Length;
}
}
}
注意:我要感谢 Stephen Cleary 的出色回答,而不是有见地的解释,并且还想强调他的解决方案中概述的建议改进,即:用封装在一行代码中的相当紧凑的解决方案替换原始 MSDN 示例中利用 WhenAny
的 unnecessary/complex 代码块,即: await Task.WhenAll(downloadTasks);
(顺便说一句,我在很多实用的应用程序中都使用了这个替代方案,特别是处理 w/multiple 股票网络查询的在线市场数据应用程序)。
非常感谢,斯蒂芬!
However, the question still remains if this implementation is capable of proper handling the possible race condition, for e.g., if the awaited and completed Task tries to access that TextBox control while the latter is still processing the update from previously completed Task?
没有竞争条件。 UI 线程一次只做一件事。
In practical sense, is WPF Dispatcher object still required to handle this potential concurrency/race condition issues in async/await multitasking implementation (or, may be, the interlocking functionality has been somehow implicitly implemented in such async/await programming construct)?
是的,但您不必明确使用它。正如我在 async
intro 中所描述的,await
关键字(默认情况下)将捕获当前上下文并在该上下文中继续执行 async
方法。 "context" 是 SynchronizationContext.Current
(如果当前 SyncCtx 是 null
,则 TaskScheduler.Current
)。
在这种情况下,它将捕获一个 UI SynchronizationContext
,它在幕后使用 WPF Dispatcher 来调度 [=33= 上的 async
方法的其余部分] 线程。
附带说明一下,我不太喜欢“Task.WhenAny
列表并在完成时从列表中删除”方法。我发现如果您通过添加 DownloadAndUpdateAsync
方法进行重构,代码会更清晰:
async Task AccessTheWebAsync(CancellationToken ct)
{
HttpClient client = new HttpClient();
// Make a list of web addresses.
List<string> urlList = SetUpURLList();
// ***Create a query that, when executed, returns a collection of tasks.
IEnumerable<Task> downloadTasksQuery =
from url in urlList select DownloadAndUpdateAsync(url, client, ct);
// ***Use ToList to execute the query and start the tasks.
List<Task> downloadTasks = downloadTasksQuery.ToList();
await Task.WhenAll(downloadTasks);
}
async Task DownloadAndUpdateAsync(string url, HttpClient client, CancellationToken ct)
{
var length = await ProcessURLAsync(url, client, ct);
resultsTextBox.Text += String.Format("\r\nLength of the download: {0}", length);
}
async Task<int> ProcessURLAsync(string url, HttpClient client, CancellationToken ct)
{
// GetAsync returns a Task<HttpResponseMessage>.
HttpResponseMessage response = await client.GetAsync(url, ct);
// Retrieve the website contents from the HttpResponseMessage.
byte[] urlContents = await response.Content.ReadAsByteArrayAsync();
return urlContents.Length;
}