Dispatcher in WPF apps implementing multiple async

2019-08-01 19:57发布

问题:

In the following MSDN example of the WPF app, which demonstrates async/await implementation of the multiple async Tasks, the Dispatcher object apparently is not used/needed, i.e. asynchronously executed Tasks seem to have direct access to the UI controls (in this case resultTextBox TextBox control - see the line resultsTextBox.Text += String.Format("\r\nLength of the download: {0}", length);). The app has been tested, performing as expected.

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? 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)?

Listing 1. MSDN article "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;
        }
    }
}

Note: I would like to thank Stephen Cleary for his excellent answer and rather insightful explanation, and also want to highlight the recommended improvement outlined in his solution, namely: replacing that unnecessary/complex code block in original MSDN example utilizing WhenAny by rather compact solution encapsulated in a single line of code, namely: await Task.WhenAll(downloadTasks); (btw, I was using this alternative in many practical apps, in particular, online market data app dealing w/multiple stocks web queries). Many thanks, Stephen!

回答1:

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?

There is no race condition. The UI thread only does one thing at a time.

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)?

It is, but you don't have to use it explicitly. As I describe in my async intro, the await keyword (by default) will capture the current context and resume executing the async method in that context. The "context" is SynchronizationContext.Current (or TaskScheduler.Current if the current SyncCtx is null).

In this case, it will capture a UI SynchronizationContext, which uses the WPF Dispatcher under the covers to schedule the remainder of the async method on the UI thread.

On a side note, I'm not a big fan of the "Task.WhenAny the list and remove from the list as they complete" approach. I find the code is much cleaner if you refactor by adding a DownloadAndUpdateAsync method:

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