delegate 与 多线程

很多时候写 windows 程序都需要结合多线程,在 .net 中用如下得代码来创建并启动一个新的线程。

public void ThreadProc();

Thread thread = new Thread( new ThreadStart( ThreadProc ) );

thread.IsBackground = true ;

thread.Start();

但是很多时候,在新的线程中,我们需要与 UI 进行交互,在 .net 中不允许我们直接这样做。可以参考 MSDN 中的描述:

“Windows 窗体 ” 使用单线程单元 (STA) 模型,因为 “Windows 窗体 ” 基于本机 Win32 窗口,而 Win32 窗口从本质上而言是单元线程。 STA 模型意味着可以在任何线程上创建窗口,但窗口一旦创建后就不能切换线程,并且对它的所有函数调用都必须在其创建线程上发生。除了 Windows 窗体之外, .NET Framework 中的类使用自由线程模型。

STA 模型要求需从控件的非创建线程调用的控件上的任何方法必须被封送到(在其上执行)该控件的创建线程。基类 Control 为此目的提供了若干方法( Invoke 、 BeginInvoke 和 EndInvoke )。 ** Invoke ** 生成同步方法调用; ** BeginInvoke ** 生成异步方法调用。

Windows 窗体中的控件被绑定到特定的线程,不具备线程安全性。因此,如果从另一个线程调用控件的方法,那么必须使用控件的一个 Invoke 方法来将调用封送到适当的线程。

正如所看到的,我们必须调用 Invoke 方法,而 BeginInvoke 可以认为是 Invoke 的异步版本。调用方法如下:

public delegate void OutDelegate( string text);

public void OutText( string text)

{

txt.AppendText(text);

txt.AppendText( "\t\n" );

}

OutDelegate outdelegate = new OutDelegate( OutText );

this .BeginInvoke(outdelegate, new object []{text});

如果我们需要在另外一个线程里面对 UI 进行操作,我们需要一个类似 OutText 的函数,还需要一个该函数的委托 delegate ,当然,这里展示的是自定义的, .net 中还有很多其他类型的委托,可以直接使用,不需要而外声明。例如: MethodInvoker 和 EventHandler ,这两种类型委托的函数外观是固定的, MethodInvoker 是 void Function() 类型的委托,而 EventHandler 是 void Function(object, EventArgs) 类型的委托,第一个不支持参数,第二中的参数类型和数量都是固定的,这两种委托可以很方便的调用,但是缺乏灵活性。请注意 BeginInvoke 前面的对象是 this ,也就是主线程。现在再介绍 Control.InvokeRequired , Control 是所有控件的基类,对于这个属性 MSDN 的描述是:

获取一个值,该值指示调用方在对控件进行方法调用时是否必须调用 Invoke 方法,因为调用方位于创建控件所在的线程以外的线程中。

该属性可用于确定是否必须调用 Invoke 方法,当不知道什么线程拥有控件时这很有用。

也就是说通过判断 InvokeRequired 可以知道是否需要用委托来调用当前控件的一些方法,如此可以把 OutText 函数修改一下:

public delegate void OutDelegate( string text);

public void OutText( string text)

{

if ( txt.InvokeRequired )

{

OutDelegate outdelegate = new OutDelegate( OutText );

this .BeginInvoke(outdelegate, new object []{text});

return ;

}

txt.AppendText(text);

txt.AppendText( "\t\n" );

}

注意,这里的函数没有返回,如果有返回,需要调用 Invoke 或者 EndInvoke 来获得返回的结果,不要因为包装而丢失了返回值。如果调用没有完成, Invoke 和 EndInvoke 都将会引起阻塞。

现在如果我有一个线程函数如下:

public void ThreadProc()

{

for ( int i = 0; i < 5; i++)

{

OutText( i.ToString() );

Thread.Sleep(1000);

}

}

如果循环的次数很大,或者漏了 Thread.Sleep(1000); ,那么你的 UI 肯定会停止响应,想知道原因吗?看看 BeginInvoke 前面的对象,没错,就是 this ,也就是主线程,当你的主线程不停的调用 OutText 的时候, UI 当然会停止响应。

与以前 VC 中创建一个新的线程需要调用 ** AfxBeginThread ** 函数,该函数中第一个参数就是线程函数的地址,而第二个参数是一个类型为 LPVOID 的指针类型,这个参数将传递给线程函数。现在我们没有办法再使用这种方法来传递参数了。我们需要将传递给线程的参数和线程函数包装成一个单独的类,然后在这个类的构造函数中初始化该线程所需的参数,然后再将该实例的线程函数传递给 Thread 类的构造函数。代码大致如下:

public class ProcClass

{

private string procParameter = "";

public ProcClass( string parameter)

{

procParameter = parameter;

}

public void ThreadProc()

{

}

}

ProcClass threadProc = new ProcClass("use thread class");

Thread thread = new Thread( new ThreadStart( threadProc.ThreadProc ) );

thread.IsBackground = true ;

thread.Start();

就是这样,需要建立一个中间类来传递线程所需的参数。

那么如果我的线程又需要参数,又需要和 UI 进行交互的时候该怎么办呢?可以修改一下代码:

public class ProcClass

{

private string procParameter = "";

private Form1.OutDelegate delg = null ;

public ProcClass( string parameter, Form1.OutDelegate delg)

{

procParameter = parameter;

this .delg = delg;

}

public void ThreadProc()

{

delg.BeginInvoke("use ProcClass.ThreadProc()", null , null );

}

}

ProcClass threadProc = new ProcClass("use thread class", new OutDelegate(OutText));

Thread thread = new Thread( new ThreadStart( threadProc.ThreadProc ) );

thread.IsBackground = true ;

thread.Start();

这里只是我的一些理解,如果有什么错误或者不当的地方,欢迎指出。

Published At
Categories with Web编程
Tagged with
comments powered by Disqus