浅谈.NET下的多线程和并行计算(八)Winform中多线程编程基础上

简介: 首先我们创建一个Winform的应用程序,在上面添加一个多行文本框和一个按钮控件,按钮的事件如下: Thread.Sleep(1000); StringBuilder sb = new StringBuilder(); for (int i = 0; i < 10000; i++) sb.Append("test"); string s = sb.ToString(); textBox1.Text = s; 首先我们可以把这个操作理解为一个非常耗时的操作,它至少占用1秒的时间。

首先我们创建一个Winform的应用程序,在上面添加一个多行文本框和一个按钮控件,按钮的事件如下:

Thread.Sleep(1000);
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 10000; i++)
    sb.Append("test");
string s = sb.ToString();
textBox1.Text = s;

首先我们可以把这个操作理解为一个非常耗时的操作,它至少占用1秒的时间。在1秒后,我们整了一个大字符串作为文本框的值,然后在标签上显示给文本框赋值这个UI渲染行为需要的时间,程序执行结果如下:

image

我们可以感受到,在点击了按钮之后整个程序的UI就卡住了,没有办法拖动没有办法改变大小,用于体验非常差。一般能想到会新建一个线程来包装这个方法,使得UI线程不被卡住:

new Thread(() =>
{
    Thread.Sleep(1000);
    StringBuilder sb = new StringBuilder();
    for (int i = 0; i < 10000; i++)
        sb.Append("test");
    string s = sb.ToString();
    textBox1.Text = s;
}).Start();

使用调试方式运行程序的话会得到如下的异常(非调试方式不会):

image

虽然我们知道这样设置:

Control.CheckForIllegalCrossThreadCalls = false;

可以屏蔽这个错误,但是在非创建控件的线程去更新控件的状态的做法会导致很多问题,比如死锁和控件部分被更新等。微软推荐我们使用Control的Invoke或BeginInvoke方法来把涉及到控件状态更新的操作让UI线程去做:

new Thread(() =>
{
    Invoke(new Action(() =>
    {
        Thread.Sleep(1000);
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < 10000; i++)
            sb.Append("test");
        string s = sb.ToString();
        textBox1.Text = s;
    }));
}).Start();

你可能会想到这么写,但是运行程序后可以发现界面依然是卡死。想一下,虽然我们新开了一个线程,但是马上又把整个代码段交给UI线程去做了,当然起不到效果。其实这个方法的工作可以分为两部分,一部分是我们数据的计算,一部分是把计算好的数据显示在界面上,我们只应该把真正和UI相关的操作放到Invoke中让UI线程去做:

new Thread(() =>
{
    Thread.Sleep(1000);
    StringBuilder sb = new StringBuilder();
    for (int i = 0; i < 10000; i++)
        sb.Append("test");
    string s = sb.ToString();
    Invoke(new Action(() =>
    {
        textBox1.Text = s;
    }));
}).Start();

再测试一次可以发现,UI在前1秒多的时间没有卡死,在最后的一点时间还是卡死了。在继续研究卡死问题之前我们来看一下,Control提供了InvokeRequired属性来让我们判断当前线程是不是UI线程,或者说当前的操作是否需要进行Invoke:

textBox1.Text = this.InvokeRequired.ToString();
new Thread(() =>
{
    textBox1.Text += Environment.NewLine + this.InvokeRequired.ToString();
    Invoke(new Action(() =>
    {
        textBox1.Text += Environment.NewLine + this.InvokeRequired.ToString();
    }));
}).Start(); 

通过非调试方式启动程序可以得到如下结果:

image

很明显:

1) 在线程外的赋值不需要Invoke(在UI线程)

2) 在线程内的赋值需要Invoke(不在UI线程)

3) 在Invoke中的赋值已经封送给UI线程,所以不需要Invoke

继续研究卡死问题,您可能会想到,Control还提供了一个BeginInvoke方法,我们来试试看:

new Thread(() =>
{
    Thread.Sleep(1000);
    StringBuilder sb = new StringBuilder();
    for (int i = 0; i < 10000; i++)
        sb.Append("test");
    string s = sb.ToString();
    BeginInvoke(new Action(() =>
    {
        textBox1.Text = s;
    }));
}).Start();

好像效果上还是没什么区别,那么Invoke和BeginInvoke的区别在哪里呢?

我们知道Windows应用程序基于消息,Windows API提供了SendMessage和PostMessage两个API,前者执行消息后返回(不经过消息管道,先于PostMessage执行),后者把消息发送到管道异步执行。Invoke和BeginInvoke从行为上来说类似这两个API,但是实际上两者都使用了PostMessage,前者使用信号量在消息执行前阻塞,达到同步的效果。我们来做一个实验:

new Thread(() =>
{
    Thread.Sleep(1000);
    StringBuilder sb = new StringBuilder();
    for (int i = 0; i < 10000; i++)
        sb.Append("test");
    string s = sb.ToString();
    Stopwatch sw = Stopwatch.StartNew();
    Invoke(new Action(() =>
    {
        textBox1.Text = s;
    }));
    MessageBox.Show(sw.ElapsedMilliseconds.ToString());
}).Start();

运行程序:

image

可以体会到,在文本框的值出现之后才出现弹出框,文本框赋值这个消息的执行过程耗时2秒。把Invoke改为BeginInvoke其它不动再执行程序:

image

明显感到弹出框先显示2秒后文本框的值出现。BeginInvoke没有阻塞后续语句的执行。因此,需要注意,如果我们在方法中使用的变量在BeginInvoke之后有修改,极有可能发生混乱。如果您使用过委托的BeginInvoke应该会知道,通常建议总是调用EndInvoke来回收资源,对于Control的EndInvoke来说,如果您不需要获取返回值的话,那么它不是必须的(来自msdn)。

现在您可能还有疑问为什么使用了BeginInvoke,UI还是卡了大概2秒,可以这么理解,我们把这么多的文字赋值到文本框中,这个UI行为是非常耗时的,不管是Invoke还是BeginInvoke最终是发送消息给UI线程处理(两者都没有使用线程池),它就是需要这么多时间,在一般情况下我们不会在UI上呈现这么多数据。

一般来说我们能做的优化是:

1) 尽量把非UI的操作使用新的线程去异步计算,不阻塞UI线程,真正需要操作UI的时候才去提交给UI线程

2) 尽量减少UI的操作复杂度,比如如果需要在UI上绘制一个复杂图形可以在内存中先创建一个位图,绘制好之后把整个位图在UI上绘制,而不是直接在UI上绘制这个图形

举个例子,UI就好象一块画布,我们要在上面画一个巨作怎么才能不过多占用这块布的时间,让大家都能用上呢?一个方法就是我们在准备颜色和画笔的时候不占着这个布,真正要去画的时候才去用,另外一个方法就是在另一块画布上先画,然后把图案采用复印的方式印到我们的主画布上。

对于大量数据的呈现,我们还可以:

1) 采用分页,只显示一部分数据,对于Windows程序的分页可能就是滚动条性质的了,在滚动条下拉的时候再去呈现当前“页”的数据

2) 即使是一页的数据,也可以一部分一部分呈现

举个例子,对于word文档的加载一般我们一打开就可以看到第一页,然后滚动块慢慢变小,页数慢慢增多,如果一开始就加载1000页的话我们可能要1分钟后才能看到第一页,如果等不及直接向后翻滚动条的话会立即加载后面的数据:

new Thread(() =>
{
    StringBuilder sb = new StringBuilder();
    for (int i = 0; i < 100; i++)
        sb.Append("test");
    string s = sb.ToString();
    for (int i = 0; i < 20; i++)
    {
        BeginInvoke(new Action(() =>
        {
            textBox1.Text += s + i;
        }));
        Thread.Sleep(10);
    }
}).Start();

设置文本框允许纵向滚动条并且运行一下这段程序可以体会到这个效果:

image

作者: lovecindywang
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。
相关文章
|
6月前
|
存储 JSON 开发工具
Visual Studio编程效率提升技巧集(提高.NET编程效率)
Visual Studio编程效率提升技巧集(提高.NET编程效率)
Visual Studio编程效率提升技巧集(提高.NET编程效率)
|
4天前
|
网络协议 C#
基于.NET WinForm开发的一款硬件及协议通讯工具
基于.NET WinForm开发的一款硬件及协议通讯工具
|
8天前
|
Java API 调度
【JavaEE】——多线程(join阻塞,计算,引用,状态)
【JavaEE】——多线程,join,sleep引起的线程阻塞,多线程提升计算效率,如何获取线程的引用和状态
|
2月前
|
前端开发 Android开发
WinForm 直接运行 Admin.NET
本文介绍了如何将 Admin.NET 以 WinForm 桌面程序模式运行,简化了手动配置 Web 服务的过程,便于演示和作为单机软件使用。通过添加特定 NuGet 包、修改 `Program.cs` 和 `Form1.cs` 文件,并调整项目配置,最终实现了在 WinForm 中嵌入 WebView 组件显示 Admin.NET 界面的效果。
26 0
WinForm 直接运行 Admin.NET
|
2月前
|
开发框架 Java .NET
.net core 非阻塞的异步编程 及 线程调度过程
【11月更文挑战第12天】本文介绍了.NET Core中的非阻塞异步编程,包括其基本概念、实现方式及应用示例。通过`async`和`await`关键字,程序可在等待I/O操作时保持线程不被阻塞,提高性能。文章还详细说明了异步方法的基础示例、线程调度过程、延续任务机制、同步上下文的作用以及如何使用`Task.WhenAll`和`Task.WhenAny`处理多个异步任务的并发执行。
|
3月前
|
传感器 数据采集 物联网
探索.NET nanoFramework:为嵌入式设备编程的新途径
探索.NET nanoFramework:为嵌入式设备编程的新途
64 7
winform .net6 和 framework 的图表控件,为啥项目中不存在chart控件,该如何解决?
本文讨论了在基于.NET 6和.NET Framework的WinForms项目中添加图表控件的不同方法。由于.NET 6的WinForms项目默认不包含Chart控件,可以通过NuGet包管理器安装如ScottPlot等图表插件。而对于基于.NET Framework的WinForms项目,Chart控件是默认存在的,也可以通过NuGet安装额外的图表插件,例如LiveCharts。文中提供了通过NuGet添加图表控件的步骤和截图说明。
winform .net6 和 framework 的图表控件,为啥项目中不存在chart控件,该如何解决?
|
5月前
|
前端开发 JavaScript 大数据
React与Web Workers:开启前端多线程时代的钥匙——深入探索计算密集型任务的优化策略与最佳实践
【8月更文挑战第31天】随着Web应用复杂性的提升,单线程JavaScript已难以胜任高计算量任务。Web Workers通过多线程编程解决了这一问题,使耗时任务独立运行而不阻塞主线程。结合React的组件化与虚拟DOM优势,可将大数据处理等任务交由Web Workers完成,确保UI流畅。最佳实践包括定义清晰接口、加强错误处理及合理评估任务特性。这一结合不仅提升了用户体验,更为前端开发带来多线程时代的全新可能。
119 1
|
5月前
|
大数据 开发工具 开发者
从零到英雄:.NET核心技术带你踏上编程之旅,构建首个应用,开启你的数字世界探险!
【8月更文挑战第28天】本文带领读者从零开始,使用强大的.NET平台搭建首个控制台应用。无论你是新手还是希望扩展技能的开发者,都能通过本文逐步掌握.NET的核心技术。从环境搭建到创建项目,再到编写和运行代码,详细步骤助你轻松上手。通过计算两数之和的小项目,你不仅能快速入门,还能为未来开发更复杂的应用奠定基础。希望本文为你的.NET学习之旅开启新篇章!
44 1
|
5月前
|
存储 C#
揭秘C#.Net编程秘宝:结构体类型Struct,让你的数据结构秒变高效战斗机,编程界的新星就是你!
【8月更文挑战第4天】在C#编程中,结构体(`struct`)是一种整合多种数据类型的复合数据类型。与类不同,结构体是值类型,意味着数据被直接复制而非引用。这使其适合表示小型、固定的数据结构如点坐标。结构体默认私有成员且不可变,除非明确指定。通过`struct`关键字定义,可以包含字段、构造函数及方法。例如,定义一个表示二维点的结构体,并实现计算距离原点的方法。使用时如同普通类型,可通过实例化并调用其成员。设计时推荐保持结构体不可变以避免副作用,并注意装箱拆箱可能导致的性能影响。掌握结构体有助于构建高效的应用程序。
144 7