C#多线程加载控件界面卡死的解决

先听一个故事:

有一个老板忙不过来,于是招一个员工去负责某些事务。这样老板就可以腾出时间处理其它事。

后来发现员工干不下去,原因是干活需要花费,没有老板的认可,财务不给批钱。这是原则。

于是老板设计了财务审批制度,要求员工涉及到报销的时候,要回公司处理审批。

结果员工干脆守着老板办公,随时征求老板的意见。于是老板还是很忙,没办法干别的事。

所以需要改善员工的工作方法,把有关财务报销的事总结并提炼出来,必要的时候才回来报销,没必要的时候不用总守着老板。于是老板终于可以时间自由了。

这个故事其实就是多线程的原理。老板就相当于主程序的UI线程,负责响应一些外来事务,员工就是线程,干一些具体耗时的工作。

我已经不是个称职的程序员了,所以高手可以略过,希望本文能给刚学习多线程编程的朋友一些启发。

曾经羡慕那些成熟大师的作品,它在执行一个耗时很长的任务时,而它的界面又是可以灵活操作的。我最初的想法,希望线程各干各的活,耗时的过程用一个独立的线程实现,而后线程之间可以完全对象化互相访问,比如跨线程访问控件。原以为这样就可以用独立的线程更新界面了,而且主线程(UI线程)不卡顿,这是多美好的憧憬。就跟故事中那个老板的最初想法一样美好。

自己一开始写多线程的时候,看了概念和文章,着手尝试的时候,但还是遇到了故事中那位老板的困惑。所以,我最后解决的方法,也跟这位老板一样:员工干自己的事,没有必要别总占用老板的时间。

关于耗时

各种I/O最耗时。比如读写磁盘,重绘界面,网络连接等。本文以更新界面为例说明。单线程的时候,一般有个单独的方法来加载界面。这是需要多线程处理的地方。

关于基础概念

进程、线程、时间片、异步、委托、代理……我建议不要往里钻,不结合实践可能一看就会,一做就废。所以这点——略!

我们只要知道,所谓UI线程,就是主进程这条线,初始化界面的线程,如果不用多线程的话,平时写的程序都是单线程的,就一个UI线程。

关于线程池

如果可能,尽量不要频繁创建和销毁线程,比较消耗资源。利用线程池。(现在硬件水平都还可以,如果是硬件资源非常宝贵的平台,恐怕从定义变量和数组就要考虑资源分配的问题了。)

如果一时理解不了,无视就行了,new thread一样用,先看到效果,再说优化。一位有名的讲师说过,任何成功的作品,都不是一次成型的,逐渐迭代完善是必然的。

关于invoke

原则:不能跨线程访问控件,要用委托和invoke。相当于故事中的财务报销制度,原则就是没有老板批准,员工不得擅自动用公司资金。

但网上的例子也仅仅是例子,真用起来照样卡。比如我们把UI线程里加载界面的方法函数,新建一个线程去异步回调它。其实用处不大的。其实后来发现这是一种混沌的想法。

sleep:有人建议大循环时不时用一下Thread.sleep(0),阻塞当前进程,让出时间片给UI线程是吧?光写这个没用的,因为当前线程太耗时了,总是刚让权就拿回,系统让它抢占,所以没戏。sleep多少秒都不行。

DoEvents:有人建议用Task.Delay()或者Application.DoEvents()取代sleep,我试过,DoEvents最明显,一是效率低,二是没解决实际问题,当处理耗时操作的时候,主界面是可以响应操作不死机,但是它会阻塞这个耗时线程,比如你按住滚动条,以为它滚动的同时加载过程还是继续走是吧?不行的,它就暂停了,你松开鼠标别动主界面它才继续。所以不建议用。

BackGround:甭想,没用的,这不是重点。

BeginInvoke:这是重点,主要看怎么用。先看看官方的解释:“在创建控件的基础句柄所在线程上异步执行委托。”这里面,所谓“在创建控件的基础句柄所在线程”也就是UI线程。就是不管在哪用invoke都是临时切换到UI线程来执行的。相当于员工要申请资金,必须回公司找老板办理。

所以,需要更新界面的时候,想当然新建一个线程,通过委托和invoke去调用它的入口方法,等于整个业务还是在UI线程执行的,所谓的新线程等于多此一举。我之前说这是混沌的想法就在于此了。相当于员工为了遵守财务制度,时刻守着老板干活,等于白招了这个员工。所以就有了解决思路:

1,这个用于更新界面的耗时方法,我们希望它在子线程中执行,不能有直接更新控件的操作(不止一次强调,这是原则,需要使用委托异步回调)。有关控件操作的部分要简化,减少跟控件的互动,尽量高效处理(其实单线程也该如此的)。而且要提出来做第2步。员工需要规范化自己的工作流程,提高效率。

2,为每一个与控件互动的操作,单独写一个方法,以便调用。切记,这个单独的方法用于更新控件,也就是必然是在UI线程中执行的。这是原则。员工需要提炼有关财务审批的环节。

3,在耗时方法中,凡是涉及到操作控件的地方,用BeginInvoke方法去异步调用上面第2步中UI线程中的方法。员工必要的时候才去找老板审批。 

4,除了上面第3步中的地方,这个耗时方法的执行进度,与UI线程是无关,直接放在新线程中执行就好了。员工干活,没必要的时候不用总麻烦老板。

执行结果终于达到效果,无论这个耗时操作多慢,主界面一点都不卡,可以拖动窗口,可以最大化最小化,可以操作滚动条,可以点击其它按钮,当然也可以点某个按钮终止当前耗时操作……相当于老板终于有时间处理其它事务,员工可以独立工作了。

然后就可以做我们想做的了。比如,执行耗时操作时,不希望那些按钮可以点,要在合适的时机控制它的Enable属性。这个要写在子线程里,用invoke去调用,写在主UI线程没用的。因为我们已经实现UI线程的完全自由了,它既然已经新开一个线程去干复杂的事了,那就只能等那个线程执行完才能恢复Enable状态。所以写在子线程里是比较合理的。

下面我把我的代码贴出来,这是我写的,整理孩子照片用的小程序,加载过程用了我说的方法,关键地方绿字做了说明,效果如期而至。不对的地方多指正。

最后给一个建议:各种命名规则见名知意不用说了,主要是方法的命名,凡是子线程里执行的方法,我都加了Thread_前缀,我觉得这有助于梳理思路,多线程编程,千万别乱。

C#多线程加载控件界面卡死的解决_第1张图片

你可能感兴趣的:(windows,c#,ui,开发语言)