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