WPF知识点全攻略15- 线程处理
使用WPF构建应用程序时,想要保证系统的流畅性、用户的体验性,处理好UI线程(主线程)与其他线程(子线程)的关系是必要的。
以最近大火的直播带货为例,镜头前主播(部分副播)的语言动作是主线程,镜头外的场控、客服等人员,各自都有一个属于自己的子线程。场控在做软硬件调试、商品上架下架、发优惠信息,临时更改产品价格、数据监测,客服处理已经出单,物流,复购,销售统计等等操作,这些不可能都交给主播来做,不然时不时的冷场就太尴尬了。要让场控、客服等在镜头外做好辅助工作,并适时的把需要展示的信息传递给主播,才能保证直播的质量。
回归代码本身,要处理线程问题,一般使用Dispatcher和BackgroundWorker来处理子线程与主线程的交互。
1、Dispatcher使用
新建一个简单的WPF项目、只放置一个Button和一个TextBlock,点击Button,使用子线程修改TextBlock的值
<StackPanel VerticalAlignment="Center" Orientation="Horizontal"> <Button Margin="10,0" Click="Button_Click" Content="变更" /> <TextBlock Name="txt" VerticalAlignment="Center" Text="1" /> </StackPanel>
private void Button_Click(object sender, RoutedEventArgs e) { Thread thread = new Thread(UpdateText); thread.Start(); } private void UpdateText() { Thread.Sleep(TimeSpan.FromSeconds(1)); //1、直接修改,报错:System.InvalidOperationException:“调用线程无法访问此对象,因为另一个线程拥有该对象。” //this.txt.Text = "New text"; //2、使用Dispatcher,把子线程的方法,丢回给主线程执行。 Dispatcher.BeginInvoke(new Action(() => { this.txt.Text = "New text"; })); }
明显 Dispatcher的使用,把子线程中需要操作主线程的部分,丢回给了主线程,这样完美结果了更改报错的问题。但这种写法不适合在其方法内部处理耗时任务,同时过多的使用Thread来处理异步方法,也会带来不易管理、处理共享数据加锁等问题。
处理简单逻辑是可以使用Dispatcher,处理较为耗时、可能牵扯共享数据或者为方便的管理子线程的启动、停止等,微软提供了BackgroundWorker组件,来安全的处理这些问题。
2、BackgroundWorker的使用
下面代码实现了一个查找指定范围整数范围内的所有素数的功能,XAML代码如下:
<Grid Margin="5"> <Grid.RowDefinitions> <RowDefinition Height="Auto" /> <RowDefinition Height="Auto" /> <RowDefinition Height="Auto" /> <RowDefinition /> <RowDefinition Height="Auto" /> </Grid.RowDefinitions> <Grid.ColumnDefinitions> <ColumnDefinition Width="Auto" /> <ColumnDefinition /> </Grid.ColumnDefinitions> <TextBlock Margin="5">起始:</TextBlock> <TextBox Name="txtFrom" Grid.Column="1" Margin="5"> 1 </TextBox> <TextBlock Grid.Row="1" Margin="5">结束:</TextBlock> <TextBox Name="txtTo" Grid.Row="1" Grid.Column="1" Margin="5"> 500000 </TextBox> <StackPanel Grid.Row="2" Grid.Column="1" Orientation="Horizontal"> <Button Name="cmdFind" Margin="5" Padding="3" Click="cmdFind_Click"> 查找素数 </Button> <Button Name="cmdCancel" Margin="5" Padding="3" Click="cmdCancel_Click" IsEnabled="False"> 停止 </Button> </StackPanel> <TextBlock Grid.Row="3" Margin="5">结果:</TextBlock> <ListBox Name="lstPrimes" Grid.Row="3" Grid.Column="1" Margin="5" /> <TextBlock Grid.Row="4" Margin="5">进度:</TextBlock> <ProgressBar Name="progressBar" Grid.Row="4" Grid.Column="1" Height="20" MinHeight="20" Margin="5" VerticalAlignment="Bottom" Maximum="100" Minimum="0" /> </Grid>
.cs中的C#代码如下:
public MainWindow() { InitializeComponent(); backgroundWorker = new BackgroundWorker(); backgroundWorker.WorkerReportsProgress = true; backgroundWorker.WorkerSupportsCancellation = true; backgroundWorker.DoWork += backgroundWorker_DoWork; backgroundWorker.RunWorkerCompleted += backgroundWorker_RunWorkerCompleted; backgroundWorker.ProgressChanged += backgroundWorker_ProgressChanged; } private BackgroundWorker backgroundWorker; private void cmdFind_Click(object sender, RoutedEventArgs e) { // 控制按钮、清空数据 cmdFind.IsEnabled = false; cmdCancel.IsEnabled = true; lstPrimes.Items.Clear(); // 获取整数方位 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; } // 使用RunWorkerAsync,在子线程中拾取所有素数 FindPrimesInput input = new FindPrimesInput(from, to); backgroundWorker.RunWorkerAsync(input); } /// <summary> /// BackgroundWorker执行方法 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void backgroundWorker_DoWork(object sender, DoWorkEventArgs e) { // 获取传入参数 FindPrimesInput input = (FindPrimesInput)e.Argument; // 开始素数拾取 int[] primes = Worker.FindPrimes(input.From, input.To, backgroundWorker); if (backgroundWorker.CancellationPending) { e.Cancel = true; return; } // 返回结果 e.Result = primes; } /// <summary> /// BackgroundWorker执行完成 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void backgroundWorker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e) { 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; //结果放入ListBox foreach (int prime in primes) { lstPrimes.Items.Add(prime); } } //重置按钮、进度 cmdFind.IsEnabled = true; cmdCancel.IsEnabled = false; progressBar.Value = 0; } /// <summary> /// BackgroundWorker执行进度 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void backgroundWorker_ProgressChanged(object sender, ProgressChangedEventArgs e) { progressBar.Value = e.ProgressPercentage;//变更进度显示 } /// <summary> /// 取消 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void cmdCancel_Click(object sender, RoutedEventArgs e) { backgroundWorker.CancelAsync(); }
Worker.cs:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
public class FindPrimesInput { public int To { get; set; } public int From { get; set; } public FindPrimesInput(int from, int to) { To = to; From = from; } } 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的DoWork事件不可操作主线程(界面对象),用来完成一些其他耗时的后台任务,RunWorkerCompleted事件把执行结果反馈给主线程,ProgressChanged事件把执行进度反馈给主线程。
简单总结一下,Dispatcher是一种简单粗暴的处理多线程问题的方式,适合业务逻辑简单线程少的场景,BackgroundWorker则是一种安全、可控的方式,非常适用于后台文件上传下载、应用更新、实时监控等与界面交互较多场景。对于其他与界面交互不多的场景,则使用.NET的线程支持(async/await等)自主实现。
WPF知识点全攻略目录:https://www.cnblogs.com/kuangxiangnice/p/11040070.html