WPF中的并行任务

时间:2017-02-09 20:33:11

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

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

  1. 用户点击按钮。
  2. 标签已更新,以显示该应用正常运行。
  3. 并行运行两个工作方法。每个worker方法都访问一个UI元素作为其算法的一部分。
  4. 完成两个工作方法后,标签会更新,以显示每个方法运行所需的时间以及应用程序运行的总时间。
  5. 当我运行程序时,我得到以下结果:

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

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

    注意:我阅读了Task-based Asynchronous Programming上的文档。它声明,"为了更好地控制任务执行或从任务返回值,您必须更明确地使用Task对象。"这就是我隐含地运行任务的原因;我没有从工人的方法中退回任何东西,我也不需要更多的控制(至少我认为我不会)。

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

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

2 个答案:

答案 0 :(得分:3)

正如大家已经说过的那样,你做的是&#34;背景&#34;在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. 您的背景&#34;代码使用Button1.Content属性,您可以再次使用Dispatcher在事件处理程序中获取该属性。之后,您可以创建一个lambda,将您的param传递给后台操作:

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

答案 1 :(得分:0)

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

那么你的代码正在做什么:

  1. 在可用的线程池线程上执行队列Work1Work2
  2. Work1Work2中,您可以为UI线程工作。
  3. 结果 - 所有计算繁重的工作都将在一个线程上执行。哪个是UI线程。

      

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

    是的,工作方法正在不同的线程上运行。另外,你是正确的&#34;某些东西迫使应用程序以同步的方式运行&#34;它是Dispatcher.Invoke marshalls工作到UI线程。这意味着作为委托传递的所有计算繁重工作将在单个线程(UI线程)上执行。在不同的线程工作中调用Worker方法正在UI线程上完成。

      

    为什么我得到了工人方法的总和&#39;总时间的时间?

    因为正在一个线程上执行worker方法体。