0

我想在WPF中實現並行任務,它似乎不工作。我有一個非常簡單的應用程序,它有一個按鈕,一個標籤,它執行以下操作:在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的文檔。它指出,「爲了更好地控制任務執行或從任務返回值,必須更明確地使用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); 
     } 
    } 
} 
+3

您要安排線程池線程擁有它安排兩個線程池線程都要求UI線程完成一大堆工作,讓線程池線程等待完成,然後讓外部線程池線程等待*完成。不要這樣做。如果你想在UI線程中運行兩件事,只需在UI線程中運行它們,不要安排3個線程池線程在那裏做任何事情,而在UI線程中做2件事情。 – Servy

+0

如果我明白你在說什麼,我應該將UI工作放在Dispatcher.Invoke()方法中,並將定時器邏輯放在Dispatcher.Invoke()調用之外? – Halcyon

+0

你不應該在後臺工作中觸摸UI。如果有的話,你應該很少使用'Dispatcher.Invoke'。從UI中獲取所需的任何數據,然後創建一些工作人員併爲其提供數據,然後返回可能計算的結果,然後在後臺工作完成後更新UI,並顯示結果。 – Servy

回答

0

其本質Dispatcher被編組工作,某些線程。你的上下文中的線程很可能將是UI線程。

那麼你的代碼是這樣做的:

  1. 隊列Work1Work2要在可用的線程池線程執行。
  2. Work1Work2你編組到UI線程。

結果 - 所有計算繁重的工作將在單個線程上執行。哪個是UI線程。

我正確地認爲應用程序正在並行運行worker方法,並且某些內容正在強制應用程序以同步方式運行?

是的,工作方法正在不同的線程上運行。此外,您確定「某些內容正在強制應用程序以同步方式運行」,即Dispatcher.Invoke將它整理到UI線程中。這意味着作爲代理傳遞的所有計算繁重的工作將在單個線程(UI線程)上執行。當從不同線程調用Worker方法時,正在UI線程上完成工作。

爲什麼我總得到工時方法總時間的總和?

因爲工作方法正文正在一個線程上執行。

2

正如大家已經說過的,你在UI線程中做了你的「背景」工作,因爲你錯誤地使用了Dispatcher對象。對於UI事件,您應該使用它只有。於是,一對夫婦的事情可以做,在你的代碼更好:

  1. 當你有一個async事件處理程序,你可以在那裏使用面向UI代碼沒有Dispatcher,剛剛從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(); 
    }