【问题标题】:Parallel Tasks In WPFWPF 中的并行任务
【发布时间】:2017-06-28 00:43:04
【问题描述】:

我正在尝试在 WPF 中实现并行任务,但它似乎不起作用。我有一个非常简单的应用程序,它有一个按钮、一个标签,它执行以下操作:

  1. 用户点击按钮。
  2. 标签已更新,表明应用正在运行。
  3. 两个工作方法并行运行。每个工作方法都会访问作为其算法的一部分的 UI 元素。
  4. 两个工作方法完成后,标签会更新,以显示每个方法运行所用的时间以及应用运行的总时间。

当我运行程序时,我得到以下结果:

Worker 1 Time: 00:00:00.2370431
Worker 2 Time: 00:00:00.5505538
Total Time: 00:00:00.8334426

这让我相信工作方法可能是并行运行的,但是某些东西以同步方式阻塞了应用程序。我认为如果它们真正并行运行,总时间应该接近最长运行的工作方法时间。我遇到的一个问题是,因为我在工作方法中访问 UI 元素,所以我需要使用 Dispatcher.Invoke() 方法。

注意:我阅读了Task-based Asynchronous Programming 上的文档。它指出,“为了更好地控制任务执行或从任务返回值,您必须更明确地使用任务对象。”这就是我隐式运行任务的原因;我没有从工作方法返回任何东西,我不需要更大的控制权(至少我认为我不需要)。

我是否正确假设应用程序正在并行运行工作方法并且某些东西正在迫使应用程序以同步方式运行?为什么我会得到总时间的工作方法时间的总和?

XAML

<Window x:Class="WpfApp1.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:WpfApp1"
        mc:Ignorable="d"
        Title="MainWindow"
        WindowStartupLocation="CenterScreen"
        ResizeMode="NoResize"
        Height="150"
        Width="300">
  <StackPanel>
    <Button x:Name="Button1"
              HorizontalAlignment="Center"
              VerticalAlignment="Center"
              Content="Click Me"
              Click="Button1_Click" />
    <Label x:Name="Label1"
           Content=" " />
  </StackPanel>
</Window>

C#

namespace WpfApp1
{
    using System.Diagnostics;
    using System.Threading.Tasks;
    using System.Windows;
    using System.Windows.Threading;

    public partial class MainWindow
    {
        private string _t1;
        private string _t2;
        private Stopwatch _sw;

        public MainWindow()
        {
            InitializeComponent();
        }

        private async void Button1_Click(object sender, RoutedEventArgs e)
        {
            _sw = new Stopwatch();
            _sw.Start();
            Label1.Content = "Working...";

            await Task.Run(() =>
            {
                Parallel.Invoke(Work1, Work2);
                Dispatcher.Invoke(UpdateLabel);
            });
        }

        private void UpdateLabel()
        {
            _sw.Stop();
            Label1.Content = $"T1: {_t1}\nT2: {_t2}\nTotal: {_sw.Elapsed}";
        }

        private void Work1()
        {
            Dispatcher.Invoke(() =>
            {
                var text = Button1.Content;
                var stopWatch = new Stopwatch();
                stopWatch.Start();
                for (var i = 0; i < 100000000; i++) { }
                _t1 = stopWatch.Elapsed.ToString();
            }, DispatcherPriority.ContextIdle);
        }

        private void Work2()
        {
            Dispatcher.Invoke(() =>
            {
                var text = Button1.Content;
                var stopWatch = new Stopwatch();
                stopWatch.Start();
                for (var i = 0; i < 250000000; i++) { }
                _t2 = stopWatch.Elapsed.ToString();
            }, DispatcherPriority.ContextIdle);
        }
    }
}

【问题讨论】:

  • 你正在调度一个线程池线程让它调度两个线程池线程,这两个线程都只是要求 UI 线程做一堆工作,让线程池线程等待它完成,并且然后让外部线程池线程等待 那些 完成。不要那样做。如果你想在 UI 线程中运行两件事,就在 UI 线程中运行它们,不要安排 3 个线程池线程坐在那里什么都不做,而你在 UI 线程中做 2 件事。
  • 如果我明白你在说什么,我应该将 UI 工作放在 Dispatcher.Invoke() 方法中,并将计时器逻辑放在 Dispatcher.Invoke() 调用之外?
  • 您根本不应该在后台工作中触摸 UI。如果有的话,你应该很少使用Dispatcher.Invoke。从 UI 获取您需要的任何数据,然后创建一些工作人员并为他们提供数据,然后返回他们可能计算的任何结果,然后在后台工作完成后使用您的结果更新 UI。

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


【解决方案1】:

正如大家已经说过的,您在 UI 线程中进行“后台”工作,因为您以错误的方式使用 Dispatcher 对象。您应该将它用于 UI 事件。因此,您可以在代码中做得更好:

  1. 由于您有一个 async 事件处理程序,您可以在没有 Dispatcher 的情况下使用面向 UI 的代码,只需将其从 await 构造中移出即可:

    private async void Button1_Click(object sender, RoutedEventArgs e)
    {
        _sw = new Stopwatch();
        _sw.Start();
        Label1.Content = "Working...";
    
        // save the context and return here after task is done
        await Task.Run(() =>
        {
            Parallel.Invoke(Work1, Work2);
        });
        // back on UI-context here
        UpdateLabel();
    }
    
  2. 您的“背景”代码使用Button1.Content 属性,您可以再次在没有Dispatcher 的情况下在事件处理程序中获取该属性。之后,您可以创建一个 lambda,并将您的参数传递给后台操作:

    private async void Button1_Click(object sender, RoutedEventArgs e)
    {
        _sw = new Stopwatch();
        _sw.Start();
        Label1.Content = "Working...";
        var buttonContent = ((Button) sender).Content
        // this will work too
        // consider to rename your button
        // var buttonContent = Button1.Content
    
        // save the context and return here after task is done
        await Task.Run(() =>
        {
            Parallel.Invoke(() => Work1(buttonContent),
                            () => Work2(buttonContent));
        });
        // back on UI-context here
        UpdateLabel();
    }
    
  3. 现在您可以再次删除Dispatcher,这一次您可以在您的Work# 方法中执行此操作(因为您现在不需要从 UI 中获取任何内容):

    private void Work1(string text)
    {
        var stopWatch = new Stopwatch();
        stopWatch.Start();
        for (var i = 0; i < 100000000; i++)
        {
             // real work here
        }
        _t1 = stopWatch.Elapsed.ToString();
    }
    
    private void Work2(string text)
    {
        var stopWatch = new Stopwatch();
        stopWatch.Start();
        for (var i = 0; i < 100000000; i++)
        {
             // real work here
        }
        _t2 = stopWatch.Elapsed.ToString();
    }
    
  4. 最后,您应该删除额外的任务创建。由于Parallel.Invoke 不会返回Task,所以不能等待,您应该将其替换为Task.WhenAll 方法,如下所示:

    private async void Button1_Click(object sender, RoutedEventArgs e)
    {
        _sw = new Stopwatch();
        _sw.Start();
        Label1.Content = "Working...";
        var buttonContent = ((Button) sender).Content
    
        var w1 = Task.Run(() => Work1(buttonContent));
        var w2 = Task.Run(() => Work2(buttonContent));
    
        await Task.WhenAll(w1, w2);
    
        // back on UI-context here
        UpdateLabel();
    }
    

【讨论】:

    【解决方案2】:

    Dispatcher 本质上是将工作编组到某个线程。您上下文中的那个线程很可能是 UI 线程。

    那么你的代码在做什么:

    1. 队列 @9​​87654322@ 和 Work2 在可用线程池线程上执行。
    2. Work1Work2 中,您可以将工作编组到 UI 线程。

    结果 - 所有计算繁重的工作都将在单个线程上执行。哪个是 UI 线程。

    我是否正确假设应用程序正在并行运行工作方法并且某些东西迫使应用程序以同步方式运行?

    是的,工作方法正在不同的线程上运行。此外,您是正确的,“某些东西正在迫使应用程序以同步方式运行”这是Dispatcher.Invoke 它编组工作到 UI 线程。这意味着作为委托传递的所有计算繁重的工作都将在单个线程(UI 线程)上执行。当从不同的线程调用 Worker 方法时,UI 线程上的工作正在完成。

    为什么我会得到总时间的工作方法时间的总和?

    因为工作方法体是在单个线程上执行的。

    【讨论】:

      猜你喜欢
      • 2018-05-30
      • 2019-06-25
      • 1970-01-01
      • 2015-08-23
      • 2017-04-29
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2013-10-30
      相关资源
      最近更新 更多