WPF 进阶 多线程(Dispathcer DispatcherObject BackgroundWorker 类)

 110 111 集   1.1h

WPF支持单线程单元(Sing唇readApartment)模型,该模型与在Windows窗体应
用程序中使用的模型非常类似。它具有以下几条核心规则:
WPF元素具有线程关联性(Thread affinity)。创建WPF元素的线程拥有所创建的元素,其
他线程不能直接与这些WPF元素进行交互(元素是在窗口中显示的WPF对象)。

具有线程关联性的WPF对象都在类层次的某个位置继承自DispatcherObject类。
DispatchelObJect类提供了少量成员,用于核实为了使用特定的对象,代码是否在正确的
线程上执行,并且(没有在正确的线程上执行)是否能切换位置。
实际上,线程运行整个应用程序并拥有所有WPF对象。尽管可使用单独的线程显示单独
的窗口,但这种设计很少硬用。

 

Dispatcher类
调度程序(dispatcher)管理在WPF应用程序中发生的操作。调度程序拥有应用程序线程,并
管理工作项队列。当应用程序运行时,调度程序接受新的工作请求,并且一次执行一个任务。
从技术角度看,当在新线第一次实例化Dispatcher0bject类的派生类时,会创建调度程
序。如果创建相互独立的线程,并用它们显示相互独立的窗口,最终将创建多个调度程序。然
而,大多数应用程序都保持简单方式,并坚持使用一个用户界面线程和一个调度程序。然后,
他们使用多线程管理数据操作和其他后台任务。

 

DispatcherObject类:
在大多情况下,不会直接与调度程序交互。但会花费大量时间使用DispatcherObJect类的
实例,因为个WPF可视化对象都继承自这个类。DispatcherObject实例是链接到调度程序的
简单对象一一换句话说,是绑定到调度程序线程的对象。


DispatcherObJect类只提供三个成员:
Dispatcher:返回管理该对象的调度程序
CheckAccess():如果代码在正确的线程上使用对象,就返回巛否则返回false
VeriftyAccess():如果代码在正确的线程上使用对象,就什么也不做,否则抛出InvalidOperauonException异常

WPF对象为保护自身会频繁调用VerifyAccess()方法。但这并不是说WPF对象会调用
VeriftyAccess()方法来响应每个操作(因为这样严重影响性能),但会足够频繁地调用该方法,从
而不可能在错误的线程中长时间使用一个对象。

        private void cmdBreakRules_Click(object sender, RoutedEventArgs e)
        {
            Thread thread = new Thread(UpdateTextWrong);
            thread.Start();
        }

        private void UpdateTextWrong()
        {
            txt.Text = "Here is some new text.";
        }

上面的代码注定会失败。UpdateTextWrong()方法将在新线程上执行,并且不允许这个新线
程访问WPF对象。TextBox对象通过调用VenfyAccess()方法捕获这一非法操作,
并抛出InvalidOperationException异常。

正确的做法是获取拥有TextBox对象的调度程序的引用(这个调度程序也拥有应
用程序中的窗口和所有其他WPF对象)。一旦访问这个调度程序,就可以调用DispatcherInvoke()
方法将一些代码封送到调度程序线程。本质上,Beginvoke()方法会将代码安排为调度程序的
任务。然后调度程序会执行这些代码。

        private void cmdFollowRules_Click(object sender, RoutedEventArgs e)
        {
            Thread thread = new Thread(UpdateTextRight);
            thread.Start();
        }


        private void UpdateTextRight()
        {
            Thread.Sleep(TimeSpan.FromSeconds(5));

            this.Dispatcher.BeginInvoke(DispatcherPriority.Normal,
                (ThreadStart) delegate()
                {
                    txt.Text = "Here is some new text.";
                }
                );
        }

Dispatcher.BeginInvoke方法具有两个参数。

第一个参数指示任务的优先级。在大多数清况下,会使用DispatcherPnontyNormaI。

BegninInvoke()方法的第二个参数是指向一个方法的委托,该方法具有希望执行的代码。这
个方法可以旌代码中的其他地方定义,也可以使用匿名方法在内部定义代码(就像在这个示例中
所做的那样)。对于简单操作,使用内联方法效果较好,例如本例只需要使用一行代码更新用户。

请记在,如果正在执行耗时的后台工作,就需要在单独的线程中执行这个操作,然后将操作
结果封送到调度程序线程(在此更新用户界面或修改共享对象)。在传递给BeginInvoke()的方法中
执行耗时的代码是不合理的。

 

BackgroundWorker类:

BackgroundWorker组件是.NET2.0版本提供的,用于简化Windows窗体应用程序中与线程
相关的问题。然而,在WPF中同样使用BackgroundWorker组件。BackgroundWorker组件为在
单独线程中运行耗时的任务提供了一种非常简单的方法。它在后台使用调度程序,并使用基于
事件的模型对封送问题进行抽象。

BackgroundWorker组件还支持另外两个功能:进度(progess)事件和取消消息。

对于这两种情况都隐藏了线程细节,以方便代码的编写。

namespace Multithreading
{
    public class Worker
    {
        public static int[] FindPrimes(int fromNumber, int toNumber)
        {
            return FindPrimes(fromNumber, toNumber, null);
        }

        public static int[] FindPrimes(int fromNumber, int toNumber, System.ComponentModel.BackgroundWorker backgroundWorker)
        {
            int[] list = new int[toNumber - fromNumber];

            // Create an array containing all integers between the two specified numbers.
            for (int i = 0; i < list.Length; i++)
            {
                list[i] = fromNumber;
                fromNumber += 1;
            }


            //find out the module for each item in list, divided by each d, where
            //d is < or == to sqrt(to)
            //if the remainder is 0, the nubmer is a composite, and thus
            //we mark its position with 0 in the marks array,
            //otherwise the number is a prime, and thus mark it with 1
            int maxDiv = (int)Math.Floor(Math.Sqrt(toNumber));

            int[] mark = new int[list.Length];


            for (int i = 0; i < list.Length; i++)
            {
                for (int j = 2; j <= maxDiv; j++)
                {

                    if ((list[i] != j) && (list[i] % j == 0))
                    {
                        mark[i] = 1;
                    }

                }

                
                int iteration = list.Length / 100;
                if ((i % iteration == 0) && (backgroundWorker != null))
                {                
                    if (backgroundWorker.CancellationPending)
                    {
                        // Return without doing any more work.
                        return null;                      
                    }

                    if (backgroundWorker.WorkerReportsProgress)
                    {
                        //float progress = ((float)(i + 1)) / list.Length * 100;
                        backgroundWorker.ReportProgress(i / iteration);
                        //(int)Math.Round(progress));
                    }
                }

            }

            //create new array that contains only the primes, and return that array
            int primes = 0;
            for (int i = 0; i < mark.Length; i++)
            {
                if (mark[i] == 0) primes += 1;

            }

            int[] ret = new int[primes];
            int curs = 0;
            for (int i = 0; i < mark.Length; i++)
            {
                if (mark[i] == 0)
                {
                    ret[curs] = list[i];
                    curs += 1;
                }
            }

            if (backgroundWorker != null && backgroundWorker.WorkerReportsProgress)
            {
                backgroundWorker.ReportProgress(100);
            }

            return ret;

        }

        
    }
}
寻找素数类

为使用BackgroundWorker,首先需要创建该类的一个实例。创建该实例的两种选择:
1.可在代码中创建BackgroundWorker对象,并用代码关联所有事件处理程序。
2.可在XAML中声明BackgroundWorker对象。这种方法的优点是可使用特性关联事件处
理程序。因为BackgroundWorder组件不是可见的WPF元素,所以不能在任意位置放置。
需要作为窗口的资源声明BackgroundWorker对象。
这两种方法是等效的。下载示例使用的是第二种方法。第一步是通过名称空间导入,

 xmlns:cm="clr-namespace:System.ComponentModel;assembly=System"

    <Window.Resources>
      <cm:BackgroundWorker x:Key="backgroundWorker"
          WorkerReportsProgress="True" WorkerSupportsCancellation="True"
          DoWork="backgroundWorker_DoWork" ProgressChanged="backgroundWorker_ProgressChanged" 
          RunWorkerCompleted="backgroundWorker_RunWorkerCompleted"></cm:BackgroundWorker>
    </Window.Resources>

在标记的Windows.Resources部分声明BackgroundWorker对象的优点是,可使用特性设置
其属性并关联事件处理程序。例如,上面是该例最后使用的BackgroundWorker标签,该标签支持
进度通知和取消操作,并为DoWork事件、ProgressChanged事件以及RunWorkerCompleted事
件关联了事件处理程序:

using System;
using System.Collections.Generic;
using System.Text;

namespace Multithreading
{
    public class FindPrimesInput
    {
        //在素数查找示例中,使用BackgroundWorker组件的第一步是创建一个自定义类,通过这个
        //自定义类向BackgroundWorker对象传递输入参数。当调用BackgroundWorker.RunWorkerAsyn«)
        //方法时,可提供任何对象,相应的对象将被传递到DoWork事件。但只能提供一个对象,所以
        //需要将结束数字和开始数字封装到一个类中,如下所示:
 
        public int To
        { get; set; }

        public int From
        { get; set; }

        public FindPrimesInput(int from, int to)
        {
            To = to;
            From = from;
        }

    }
}

 

namespace Multithreading
{
    public class Worker
    {
        public static int[] FindPrimes(int fromNumber, int toNumber)
        {
            return FindPrimes(fromNumber, toNumber, null);
        }

        public static int[] FindPrimes(int fromNumber, int toNumber, System.ComponentModel.BackgroundWorker backgroundWorker)
        {
            int[] list = new int[toNumber - fromNumber];

            // Create an array containing all integers between the two specified numbers.
            for (int i = 0; i < list.Length; i++)
            {
                list[i] = fromNumber;
                fromNumber += 1;
            }


            //find out the module for each item in list, divided by each d, where
            //d is < or == to sqrt(to)
            //if the remainder is 0, the nubmer is a composite, and thus
            //we mark its position with 0 in the marks array,
            //otherwise the number is a prime, and thus mark it with 1
            int maxDiv = (int)Math.Floor(Math.Sqrt(toNumber));

            int[] mark = new int[list.Length];


            for (int i = 0; i < list.Length; i++)
            {
                for (int j = 2; j <= maxDiv; j++)
                {

                    if ((list[i] != j) && (list[i] % j == 0))
                    {
                        mark[i] = 1;
                    }

                }

                //FindPrimes()方法每完成1%的工作就报告一次,使用的代码如下所示:
                int iteration = list.Length / 100;
                if ((i % iteration == 0) && (backgroundWorker != null))
                {
                    //当调用CancelAsync()方法时不会自动发生任何操作。相反,执行任务的代码需要显式地检
                    //查取消请求,执行所有清除操作,然后返回。下面的FindPnmes()方法中的代码在即将报告进
                    //度之前检查取消请求:

                    if (backgroundWorker.CancellationPending)
                    {
                        // Return without doing any more work.
                        return null;                      
                    }

                    if (backgroundWorker.WorkerReportsProgress)
                    {
                        //float progress = ((float)(i + 1)) / list.Length * 100;
                        backgroundWorker.ReportProgress(i / iteration);
                        //(int)Math.Round(progress));
                    }
                }

            }

            //create new array that contains only the primes, and return that array
            int primes = 0;
            for (int i = 0; i < mark.Length; i++)
            {
                if (mark[i] == 0) primes += 1;

            }

            int[] ret = new int[primes];
            int curs = 0;
            for (int i = 0; i < mark.Length; i++)
            {
                if (mark[i] == 0)
                {
                    ret[curs] = list[i];
                    curs += 1;
                }
            }

            if (backgroundWorker != null && backgroundWorker.WorkerReportsProgress)
            {
                backgroundWorker.ReportProgress(100);
            }

            return ret;

        }

        
    }
}
using System;
using System.Collections.Generic;
using System.Text;
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.Shapes;
using System.ComponentModel;

namespace Multithreading
{
    /// <summary>
    /// Interaction logic for BackgroundWorkerTest.xaml
    /// </summary>

    public partial class BackgroundWorkerTest : System.Windows.Window
    {
        public BackgroundWorkerTest()
        {
            InitializeComponent();
            //为在代码中访问该资源,需要将它从Resources集合中提取出来。在本例中,窗口在其构造
            //函数中执行该步骤,使所有事件处理代码都能更容易地访问该资源:
            backgroundWorker = ((BackgroundWorker)this.FindResource("backgroundWorker"));
        }

        private BackgroundWorker backgroundWorker;
        /// <summary>
        /// 为运行BackgroundWorker对象,需要调用BackgroundWorker.RunWorkerAsync()方法,并
         /// 传入FindPnmes1nput对象。当用户单击FindPrimes按钮时,下面的代码将完成这一工作:
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void cmdFind_Click(object sender, RoutedEventArgs e)
        {
            // Disable the button and clear previous results.
            cmdFind.IsEnabled = false;
            cmdCancel.IsEnabled = true;
            lstPrimes.Items.Clear();            

            // Get the search range.
            int from, to;
            if (!Int32.TryParse(txtFrom.Text, out from))
            {
                MessageBox.Show("Invalid From value.");
                return;
            }
            if (!Int32.TryParse(txtTo.Text, out to))
            {
                MessageBox.Show("Invalid To value.");
                return;
            }

            // Start the search for primes on another thread.
            FindPrimesInput input = new FindPrimesInput(from, to);
            backgroundWorker.RunWorkerAsync(input);
        }
        /// <summary>
        /// 当BackgroundWorker对象开始执行后,从CLR线程池提取一个自由线程,然后从这个线
        /// 程引发DoWork事件。您可以处理DoWork事件并开始执行耗时的任务,但切勿访问共享数据(如
        ///窗口类中的字段)或用户界面对象。一旦完成工作,对象就会引发RunWorker-
        ///Completed事件以通知应用程序。这个事件在调度程序线程引发,在该线程上您可以访问共享
        ///数据和用户界面,而不会导致任何问题。
        ///一旦BackgroundWorker对象请求到线程,就引发DoWork事件。可通过处理这个事件来调
        /// 用Worker.FindPrimes()方法。DoWork事件提供一个DoWorkEventArgs对象,该对象是检索和
        ///返回信息的要素。可通过DoWorkEventArgs.Argument属性检索输入对象,并通过设置DoWork-
        ///EventArgs、Resu1t属性返回结果。
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void backgroundWorker_DoWork(object sender, DoWorkEventArgs e)
        {
            // Get the input values.
            FindPrimesInput input = (FindPrimesInput)e.Argument;

            // Start the search for primes and wait.
            int[] primes = Worker.FindPrimes(input.From, input.To, backgroundWorker);
            
            //DoWork事件处理程序中的代码还需要显式地将DoWorkEventArgs℃ancel属性设置为e,
            //以完成取消操作。然后用户可以从方法中返回,而不必试图构建素数字符串。
            if (backgroundWorker.CancellationPending)
            {
                e.Cancel = true;
                return;
            }

            // Return the result.
            e.Result = primes;
        }
        /// <summary>
        /// 一旦Worker.FindPrimes()方法执行完毕,BackgroundWorker对象就在调度程序线程引发
        /// RunWorkerCompletedEventArgs事件。这时,可通过RunWorkerCompIetedEventArgs.ResuIt属性
        /// 检索结果。然后可更新界面并访问窗口级别的变量,而不必担心任何问题。
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void backgroundWorker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
        {
            //DoWork事件处理程序中的代码还需要显式地将DoWorkEventArgs℃ancel属性设置为true
            //以完成取消操作。然后用户可以从方法中返回,而不必试图构建素数字符串。
            if (e.Cancelled)
            {
                MessageBox.Show("Search cancelled.");
            }
            else if (e.Error != null)
            {
                // An error was thrown by the DoWork event handler.
                MessageBox.Show(e.Error.Message, "An Error Occurred");
            }
            else
            {
                int[] primes = (int[])e.Result;
                foreach (int prime in primes)
                {
                    lstPrimes.Items.Add(prime);
                }                
            }
            cmdFind.IsEnabled = true;
            cmdCancel.IsEnabled = false;
            progressBar.Value = 0;
        }
        /// <summary>
        /// BackgroundWorker类还为跟踪进度提供了内置支持。在长时间运行的任务中,如果需要持
        ///续向客户端发送己经完成的工作量的信息,这一支持是很有用的。
        ///要为进度添加支持,首先需要将BackgroundWorker.WorkerReportsProgress属性设置为true。
        ///实际上,提供和显示执行进度信息是两个步骤。首先,DoWork事件处理代码需要调用
        /// BackgroundWorker.ReportProgress()方法,并提供己经完成的百分比(从0%到100%)。可根据个
        ///人喜好或多或少地执行该工作。每次调用R印proess() 方法时,BackgroundWorker对象都会
        ///Changed事件是从用户界面线程引发的,所以不需要使用Dispatcher.Begin1nvoke()方法。
        ///FindPnmes() 方法每完成1%的工作就报告一次,使用的代码如下所示:
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void backgroundWorker_ProgressChanged(object sender, ProgressChangedEventArgs e)
        {
            progressBar.Value = e.ProgressPercentage;
        }
        /// <summary>
        /// 使用BackgroundWorker对象为长时间执行的任务添加取消支持同样也很容易。第一步是
        /// 将BackgroundWorker.WorkerSuppoflsCanceIIation属性设置为恤re。
        /// 为请求取消,代码需要调用BackgroundWorker℃ancelAsync()方法。在该例中,当单击cancel
        /// 按钮时请求取消:

        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void cmdCancel_Click(object sender, RoutedEventArgs e)
        {
            backgroundWorker.CancelAsync();
        }
    }
}
<Window x:Class="Multithreading.BackgroundWorkerTest"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Title="Multithreading" Height="323.2" Width="305.6"
    xmlns:cm="clr-namespace:System.ComponentModel;assembly=System"
    >
    <Window.Resources>
      <cm:BackgroundWorker x:Key="backgroundWorker"
          WorkerReportsProgress="True" WorkerSupportsCancellation="True"
          DoWork="backgroundWorker_DoWork" ProgressChanged="backgroundWorker_ProgressChanged" 
          RunWorkerCompleted="backgroundWorker_RunWorkerCompleted"></cm:BackgroundWorker>
    </Window.Resources>
    <Grid Margin="5">
      <Grid.RowDefinitions>
        <RowDefinition Height="Auto"></RowDefinition>
        <RowDefinition Height="Auto"></RowDefinition>
        <RowDefinition Height="Auto"></RowDefinition>
        <RowDefinition></RowDefinition>
        <RowDefinition Height="Auto"></RowDefinition>
      </Grid.RowDefinitions>
      <Grid.ColumnDefinitions>
        <ColumnDefinition Width="Auto"></ColumnDefinition>
        <ColumnDefinition></ColumnDefinition>
      </Grid.ColumnDefinitions>
      
      
      <TextBlock Margin="5">From:</TextBlock>
      <TextBox Name="txtFrom" Grid.Column="1" Margin="5">1</TextBox>
      <TextBlock Grid.Row="1" Margin="5">To:</TextBlock>
      <TextBox Name="txtTo" Grid.Row="1" Grid.Column="1" Margin="5">500000</TextBox>

      <StackPanel Orientation="Horizontal"
              Grid.Row="2" Grid.Column="1">      
      <Button Name="cmdFind"
              Margin="5" Padding="3"
              Click="cmdFind_Click">Find Primes</Button>
      <Button Name="cmdCancel"
              Margin="5" Padding="3" IsEnabled="False"
              Click="cmdCancel_Click">Cancel</Button>
      </StackPanel>
      
      <TextBlock Grid.Row="3" Margin="5">Results:</TextBlock>
      <ListBox Name="lstPrimes" Grid.Row="3" Grid.Column="1" 
               Margin="5"></ListBox>
      

      <ProgressBar Name="progressBar"
               Grid.Row="4" Grid.ColumnSpan="2"
               Margin="5" VerticalAlignment="Bottom" MinHeight="20" 
               Minimum="0" Maximum="100" Height="20"></ProgressBar>
    </Grid>
</Window>

 

不需要任何加锁代码,也不需要使用D1spatcher.BeginInvoke()方法。BackgroundWorker
对象自动解决了这些问题。

 

posted @ 2021-02-27 18:59  KnowledgePorter  阅读(291)  评论(0)    收藏  举报