多线程的问题和一些学习感悟

写一个小软件的时候碰到了一个问题。有一串很耗时的函数需要处理。基本流程如下:

private void Sample()
{
    aMethodNeedLongTime();//一个很耗时的计算函数
    aMethodNeddLongTimeRefWithUI();//一个很耗时的与UI控件处理相关的函数
}

很显然,执行这个函数界面会无法响应。为了解决这个问题,我几乎是不假思索的写下了下面这样的代码:

private void Sample()
{
    Thread oneStep=new Thread(new ThreadStart(aMethodNeedLongTime);
    oneStep.Start();
    oneStep.Join();

    aMethodNeddLongTimeRefWithUI();//UI相关的内容是无法这样使用多线程的,这个问题等会再说
}

效果没有任何变化,界面在计算过程中依然没有响应。仔细想下,发现了问题。我开了一个新的线程,然后马上用它堵塞了主线程,使得主线程无法与新的进程并行工作,所以界面不会有响应。这是一个很低级的错误。因为这里错误的理解了多线程的用途。多线程适用于并行处理事件,使不同线程上的多个事件可以同时响应,而我用了串行的思想应用了它,使得多线程并行变成了多线程轮流执行,很是汗颜,真是枉费了我学那么久的操作系统了。
但仔细回忆下,我自所以如此自然的写下了上面的代码,是因为在公司实习的时候,某前辈的代码就是这样写的。那时一个Console的程序,在Main函数中前辈用了大量的类似上面的代码来处理极其耗时数据库操作函数。代码类似于:

static void Main(string[] args)
{
    Thread oneStep=new Thread(new ThreadStart(aMethodNeedLongTime);
    oneStep.Start();
    oneStep.Join();
    ...
}

而且实际效果颇有多线程的风范,在数据库操作时,Console窗口可以可以自由的移动。现在仔细想想,可能是因为这是Console程序,并没有启动一个Application。而Console窗口的绘制另有线程负责,这是与通常的WinForm程序不同的。(不知道想的对不对)也就是直接使用:
static void Main(string[] args)
{
    aMethodNeedLongTime();
    ...
}
效果比上面的效果还要好(至少省去了开一个线程的时间)。
但是最初看到这样代码的时候,我遗忘了思考。看到了效果,却忘了仔细想想达到这种效果的实际原因,而只是简单的相信了所谓的前辈写的代码。这种态度很要不得。人都说,做事需要经验,我想经验是在思考中得来的,经历过的东西不能就这样让他在手边白白流走,甚至留下了一大堆的垃圾。改正,下次不再放过理解不了的疑点,不再偏信任何无法理解的东西。不过让我比较后怕的事,就这样的代码竟然成天在DELL的机器中运行着(我们给DELL做外包)。我反正是没脸回公司了,怕我的继任者拍着我的头说,孩子,这种代码是你写的吧。哎,真太汗了。
言归正传。为了解决这个问题,上csdn请教了一些前辈。很多人说,仅是为了解决界面响应问题,不需要使用多线程,而是利用Application.DoEvent()来做。Application.DoEvent()是沿用的VB的一贯做法(再汗一个,就我这样的实在无法说是用过VB啊)。在耗时函数(比如上面的aMethodNeedLongTime)中的某个部位嵌入Application.DoEvent()(一般是嵌在循环体中,使其可以定时执行)。其作用是通知Application先去处理一下队列中的其他事件,再来处理耗时的运算。但我无法将其嵌入到我的aMethodNeedLongTime中去(这种情况很多,也许这个方法被封装了,也许方法中没有循环体可以插入,也许和我一样讨厌插入不伦不类的东西)。于是我用了一个Timer来定时处理。代码如下:
private void Sample()
{
    timer1.Enable=true;

    aMethodNeedLongTime;

    timer1.Enable=false;
}
private void Timer1_Tick(object sender,EventArgs e)
{
    Application.DoEvent();
}
很出乎我意料,这段代码竟然基本可以达到目的。我本以为这一个是个死循环。因为我以为Tick也需要在事件队列中排队,会同样被耗时运算堵塞无法执行,因此这段代码没有效果。但实际上,界面基本达到了可以正常晃动的程度。也就是说可能Tick事件另有线程来处理,也可能Tick事件优先级较高,可以提前得到处理。在msdn中没查到相关的解释,期待某个有研究的前辈可以指点。
也有人让我继续使用多线程来处理。串行的处理,可以利用轮询,通信(这是我想的,还没有具体学习)等来解决。但更直接的解决办法是把上面的aMethodNeedLongTime()和aMethodNeddLongTimeRefWithUI()放在一个线程中执行。但是这里的aMethodNeddLongTimeRefWithUI()是一个与UI控件相关的方法,通常UI控件是不允许其他线程操作的,其原因,愚翁(一个csdn上C#版的四星名人)在其blog中是这样说的:因为自定义的线程和UI线程是在不同地方建立的。有一些感性的认识,但不是很理性,等我问清楚以后再写好了。当然如果一定要做的话,还是有方法的(不然进度条都不用做了)。利用invoke函数。这个问题微软说的很烂。在愚翁的Blog中有很好的说明,参见:
http://blog.csdn.net/Knight94/archive/2006/05/27/757351.aspx
http://blog.csdn.net/knight94/archive/2006/03/16/626584.aspx
利用这个,我终于把两个函数仍在了一个线程中执行。可以看出来,整个流程是一个很固定的模式。于是,自然会有人帮我们做好组件。这个人就是微软。在VS2005中(其实是.net2.0),添加了一个BackgroudWorker的组件,这个组件专门用于处理异步的,大运算量的事件。它会帮你开好新的线程。这一次微软的文档写的还算不错,可以参见:
ms-help://MS.VSCC.v80/MS.MSDN.v80/MS.VisualStudio.v80.chs/dv_fxmclictl/html/5b56e2aa-dc05-444f-930c-2d7b23f9ad5b.htm
ms-help://MS.VSCC.v80/MS.MSDN.v80/MS.NETDEVFX.v20.chs/cpref3/html/T_System_ComponentModel_BackgroundWorker.htm
一些个人的说明就是,虽然文档中善意的提醒:您必须非常小心,确保在 DoWork 事件处理程序中不操作任何用户界面对象。而应该通过 ProgressChanged 和 RunWorkerCompleted 事件与用户界面进行通信。但实际上,我们还是可以利用invoke将UI对象的处理逻辑也添加到DoWork事件中去。还有就是如果需要处理Cancel或进度条,要将WorkerReportsProgress和WorkerSupportsCancellation属性设为True(默认是false)。而具体的Cancel和进度运算需要在DoWork中的大运算量函数中嵌入相关的逻辑。所以没有特别需求可以不使用。
问题至此,算是初步解决了,虽然还留下了一堆的尾巴。但还有许多新的问题需要处理。比如在线程后台处理是,前台的事件如何控制,多线程对性能的影响,等等。总之,又想起那句老话,掌握一种力量很容易,但学会使用这种力量却很困难。多多思考和积累解决思路,多多学会权衡和取舍也许才更为重要。

posted on 2006-07-28 18:06  duguguiyu  阅读(2325)  评论(9编辑  收藏  举报

导航