【问题标题】:Dispatcher in WPF apps implementing multiple async TasksWPF 应用程序中的调度程序实现多个异步任务
【发布时间】:2016-07-28 15:46:27
【问题描述】:

在下面的 WPF 应用程序的 MSDN 示例中,它演示了多个异步 Tasksasync/await 实现,Dispatcher 对象显然没有使用/不需要,即异步执行 Tasks 似乎有直接访问 UI 控件(在本例中为 resultTextBox TextBox 控件 - 参见 resultsTextBox.Text += String.Format("\r\nLength of the download: {0}", length); 行)。该应用已经过测试,性能符合预期。

但是,问题仍然存在此实现是否能够正确处理可能的竞争条件,例如,如果等待和完成的Task 尝试访问TextBox 控制而后者仍在处理来自先前完成的Task 的更新?实际上,是否仍需要 WPF Dispatcher 对象来处理 async/await 多任务实现中的这种潜在的并发/竞争条件问题(或者,可能是,联锁功能已经以某种方式隐式实现async/await 编程结构)?

清单 1。 MSDN 文章“启动多个异步任务并在完成时对其进行处理”(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,即:await Task.WhenAll(downloadTasks);(顺便说一句,我在许多实际应用中都使用了这个替代方案应用程序,尤其是处理多股网络查询的在线市场数据应用程序)。 非常感谢,斯蒂芬!

【问题讨论】:

    标签: c# wpf async-await task-parallel-library dispatcher


    【解决方案1】:

    但是,这个实现是否能够正确处理可能的竞争条件仍然存在问题,例如,如果等待和完成的任务尝试访问该 TextBox 控件,而后者仍在处理来自先前完成的任务的更新?

    没有竞争条件。 UI 线程一次只做一件事。

    实际上,在异步/等待多任务实现中是否仍需要 WPF 调度程序对象来处理这种潜在的并发/竞争条件问题(或者,可能是,在这种异步/等待编程结构中以某种方式隐式实现了联锁功能) ?

    可以,但您不必明确使用它。正如我在async intro 中所描述的,await 关键字(默认情况下)将捕获当前上下文并在该上下文中继续执行async 方法。 “上下文”是 SynchronizationContext.Current(如果当前 SyncCtx 是 null,则为 TaskScheduler.Current)。

    在这种情况下,它将捕获 UI SynchronizationContext,它使用 WPF Dispatcher 在后台调度 async 方法的其余部分在 UI 线程上。

    附带说明,我不是“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;
    }
    

    【讨论】:

    • Stephen,非常感谢您详细而富有洞察力的解释!结合您的优秀出版物,它有助于更​​好地理解 async/await 结构的细微差别。关于第二个相关问题(即WhenAny),似乎“伟大的思想是相同的”:))-在我的实际实现中,我使用WhenAll(而不是整个“while”块)并添加一行(resultsTextBox.Text + = String.Format("\r\n下载长度:{0}", urlContents.Length);) 到 ProcessURL() 方法(仅返回 Task 而不是 Task)。谢谢和问候,
    猜你喜欢
    • 2018-06-27
    • 2019-08-11
    • 1970-01-01
    • 2021-09-04
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2023-03-13
    • 1970-01-01
    相关资源
    最近更新 更多