极品分享

C# WPF 新线程耗时操作之Dispatcher.BeginInvoke()方法使用不当导致UI界面卡死的原因分析

前段时间,公司同事开发了一个小工具,在工具执行过程中,UI界面一直处于卡死状态。

通过阅读代码发现,主要是由于Dispatcher.BeginInvoke()方法使用不当导致的。

本文将通过一个WPF模拟程序来演示一下界面卡死的现象,并通过修改代码来解决界面卡死的问题。

希望通过对本文的学习,大家能对Dispatcher.BeginInvoke()方法有一个新的认识。


案例一:新线程进行耗时数学计算,委托更新UI。


示例WPF程序,用来计算1~n的和值,这里的n可以是1亿~25 亿之间的某个值,通过界面录入,结果显示在n输入框后面的文本框中,既然是WPF程序,代码包含xaml及cs代码两部分,本文一并给出。

以下为cs代码:

using System;  
using System.Windows;  
using System.Threading;  
  
namespace DispatcherExample  
{  
    /// <summary>  
    /// MainWindow.xaml 的交互逻辑  
    /// </summary>  
    public partial class MainWindow : Window  
    {  
        public MainWindow()  
        {  
            InitializeComponent();  
        }  
  
        private void button1_Click(object sender, RoutedEventArgs e)  
        {  
            Int64 inputNumber;  
            if (!Int64.TryParse(this.textBox1.Text, out inputNumber))  
            {  
                MessageBox.Show("请输入1亿-10亿皑间的整型数据!");  
                return;  
            }  
            if (inputNumber > 2500000000 || inputNumber<100000000)  
            {  
                MessageBox.Show("请输入1亿-10亿间的整型数据!");  
                return;  
            }  
            Thread newThread = new Thread(new ParameterizedThreadStart(GetResult));  
            newThread.Start(inputNumber);  
        }  
  
        private void GetResult(object inputNumber)  
        {  
            this.Dispatcher.BeginInvoke((Action)delegate()  
            {  
                this.textBox2.Text = CalcSum((Int64)inputNumber).ToString();  
            });  
        }  
  
        private double CalcSum(Int64 inputNumber)  
        {  
            double sum=0;  
            for (int i = 0; i < inputNumber; i++)  
            {  
                sum +=i;  
            }  
            return sum;  
        }  
    }  
}


以下为xaml代码:


<Window x:Class="DispatcherExample.MainWindow"  
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"  
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"  
        Title="求(和)你亿万次~~" Height="350" Width="525" ResizeMode="NoResize">  
    <Grid>  
        <Grid.ColumnDefinitions>  
            <ColumnDefinition Width="252*" />  
            <ColumnDefinition Width="251*" />  
        </Grid.ColumnDefinitions>  
        <Button Content="计算和值" Height="23" HorizontalAlignment="Left" Margin="213,168,0,0" Name="button1" VerticalAlignment="Top" Width="75" Click="button1_Click" Grid.ColumnSpan="2" />  
        <Label Content="输入1亿-25亿间的数字:" Height="28" HorizontalAlignment="Left" Margin="36,93,0,0" Name="label1" VerticalAlignment="Top" />  
        <TextBox Height="23" HorizontalAlignment="Left" Margin="158,96,0,0" Name="textBox1" VerticalAlignment="Top" Width="120" Grid.ColumnSpan="2" />  
        <TextBox Height="23" HorizontalAlignment="Left" Margin="35,96,0,0" Name="textBox2" VerticalAlignment="Top" Width="177" Text="结果看这里..." Grid.Column="1" />  
    </Grid>  
</Window>


执行程序,界面如下:


1-1.20130915175906859.png



输入2500000000,点击“计算和值”按钮,程序开始计算和值,界面卡死,无法再操作该程序(如移动位置或重新输入等)。

分析代码,发现问题应该出在下面的代码中,因为该部分代码中存在调用UI主线程的操作,此种操作不当往往会导致界面卡死的现象。


private void GetResult(object inputNumber)  
{  
     this.Dispatcher.BeginInvoke((Action)delegate()  
     {  
           this.textBox2.Text = CalcSum((Int64)inputNumber).ToString();  
     });  
}


那么,问题到底出在哪里呢?

要想弄清楚这点,还得了解一下Dispatcher.BeginInvoke()方法。

MSDN上对Dispatcher.BeginInvoke方法的解释如下:

Dispatcher.BeginInvoke 方法 (Action)

在与 Dispatcher关联的线程上异步执行指定的委托。 

那么本实例中,与 Dispatcher关联的线程是什么呢?

要想弄清楚这点很简单。只要知道this.Dispatcher.BeginInvoke()中的this指的是什么就可以了。在Visual studio中将鼠标至于this上,发现this指的是当前的窗体类(如下图),即程序的主线程。


1-2.20130915180303484.png



到这,我们应该知道问题出在哪里了。

原因是:在GetResult()方法中,将求和的操作交由主线程来完成,当计算未完成时,界面自然会被卡死。

通过与同事交谈了解到,他其实想要的是:新开一个线程来完成自己预想的运算(类似于示例程序中的求和运算),在结果出来后再调用主线程显示结果。

这样界面就不会出现卡死现象,但是上面的代码并没有达到预想结果。

原因前面已经交代了,因为这段代码将求和的计算仍然丢给了主线程,尽管新开了线程,但是新开线程并不进行求和运算,可以说是绕了一圈又回来了。

主线程开新线程,新线程又调用主线程。这有点像工作中的踢皮球,我给你一件事,你说不会,又踢回给我。

找到原因再修改就简单了,修改后的代码如下:


private void GetResult(object inputNumber)  
{  
     double result=CalcSum((Int64)inputNumber);  
     this.Dispatcher.BeginInvoke((Action)delegate()  
     {  
           //this.textBox2.Text = CalcSum((Int64)inputNumber).ToString();  
           this.textBox2.Text = result.ToString();            
     });  
}


至于为什么要这样修改,我想:你懂的。

再次执行程序,输入2500000000,求和,界面不再存在卡死现象。


原因分析:

this.textBox2.Text = CalcSum((Int64)inputNumber).ToString();  中的CalcSum((Int64)inputNumber)方法是耗时运算操作,该计算操作的过程放在了委托更新UI当中了,

UI的更新必须等待该委托内调用的该方法执行完成后才能执行完成该委托完成UI更新,也就造成了主进程的阻塞。

把计算方法的计算调用放在委托更新UI的外面

double result=CalcSum((Int64)inputNumber);

也就避免了对主进程的阻塞。


案例二:新线程进行耗时数据库数据读取,委托更新UI。


由于SQLServer放在了公网服务器上,而且没有做WEBService或WCF等,采用的是C/S客户端直连数据库的方式。

在公网数据读取的过程中非常耗时,界面卡死现象明显。所以想到了用新线程读取数据,委托更新UI。由于使用不当未达到预期的效果。


问题代码如下:


public void Init_LoadData_SearchKeys(string iSearchKeys)
{
	try
	{
	    //清空DataGrid中数据
	    dgList.ItemsSource = null;
	    //显示提示框
	    tbLoading_Notice.Visibility = Visibility.Visible;
	    //开启新线程载入数据委托异步更新UI
	    this.Dispatcher.BeginInvoke((Action)delegate()  
	    {
	        //载入分页数据
		this.PageNoBar.ShowPages(this.dgList, new UserBLL().GetAll_DataTable(iSearchKeys), 19);
		//关闭提示框
		tbLoading_Notice.Visibility = Visibility.Hidden;
	    });
	}
	catch (Exception ex)
	{
	    MessageBox.Show(ex.ToString());
	}
}


修改后代码如下:


public void Init_LoadData_SearchKeys(string iSearchKeys)
{
    //清空DataGrid中数据
    dgList.ItemsSource = null;
    //显示提示框
    tbLoading_Notice.Visibility = Visibility.Visible;
    //开启新线程执行获取数据和委托UI更新
    Task taskA = Task.Factory.StartNew(() =>
    {
	Init_LoadData_FecthSearchKeys(iSearchKeys);
    });

}

private void Init_LoadData_FecthSearchKeys(string iSearchKeys)
{
    //从数据库读取数据获得DataTale
    DataTable iDataTable = new InspectionBLL().GetAll_DataTable(iSearchKeys);
    //委托异步更新UI
    this.Dispatcher.BeginInvoke((Action)delegate()
    {
	//载入分页数据
	this.PageNoBar.ShowPages(this.dgList, iDataTable, 19);
	//关闭提示框
	this.tbLoading_Notice.Visibility = Visibility.Hidden;
    });  
}


原因分析:

new UserBLL().GetAll_DataTable(iSearchKeys)方法是耗时操作,该操作放在了委托UI更新的Dispatcher.BeginInvoke()中了,也就造成了主进程的阻塞。

将其耗时操作的方法放在委托更新UI的Dispatcher.BeginInvoke()外面执行,然后给委托UI更新用执行的结果也就避免了对主进程的阻塞。


2017-04-29 0 /
NET学习
/
标签: 

评论回复

回到顶部