当前位置:城玮文档网 >作文大全 > CWinForm多线程开发

CWinForm多线程开发

时间:2022-07-22 15:10:03 来源:网友投稿

 C# WinForm 多线程开发 一 一 Thread 类库

 Windows 是一个多任务的系统,如果你使用的是 windows 2000 及其以上版本,你可以通过任务管理器查看当前系统运行的程序和进程。什么是进程呢?当一个程序开始运行时,它就是一个进程,进程所指包括运行中的程序和程序所使用到的内存和系统资源。而一个进程又是由多个线程所组成的,线程是程序中的一个执行流,每个线程都有自己的专有寄存器(栈指针、程序计数器等),但代码区是共享的,即不同的线程可以执行同样的函数。多线程是指程序中包含多个执行流,即在一个程序中可以同时运行多个不同的线程来执行不同的任务,也就是说允许单个程序创建多个并行执行的线程来完成各自的任务。

 一 关于 Thread 的说明

 在.net framework class library 中,所有与多线程机制应用相关的类都是放在 System.Threading命名空间中的。其中提供 Thread 类用于创建线程,ThreadPool 类用于管理线程池等等,此外还提供解决了线程执行安排,死锁,线程间通讯等实际问题的机制。如果你想在你的应用程序中使用多线程,就必须包含这个类。Thread 类有几个至关重要的方法,描述如下:

 Start():启动线程

 Sleep(int):静态方法,暂停当前线程指定的毫秒数

 Abort():通常使用该方法来终止一个线程

 Suspend():该方法并不终止未完成的线程,它仅仅挂起线程,以后还可恢复。

 Resume():恢复被 Suspend()方法挂起的线程的执行

 线程入口使程序知道该让这个线程干什么事,在 C#中,线程入口是通过 ThreadStart 代理(delegate)来提供的,你可以把 ThreadStart 理解为一个函数指针,指向线程要执行的函数,当调用 Thread.Start()方法后,线程就开始执行 ThreadStart 所代表或者说指向的函数。

 ThreadState 在各种情况下的可能取值如下:

 Aborted:线程已停止

 AbortRequested:线程的 Thread.Abort()方法已被调用,但是线程还未停止

 Background:线程在后台执行,与属性 Thread.IsBackground 有关

 Running:线程正在正常运行

 Stopped:线程已经被停止

 StopRequested:线程正在被要求停止

 Suspended:线程已经被挂起(此状态下,可以通过调用 Resume()方法重新运行)

 SuspendRequested:线程正在要求被挂起,但是未来得及响应

 Unstarted:未调用 Thread.Start()开始线程的运行

 WaitSleepJoin:线程因为调用了 Wait(),Sleep()或 Join()等方法处于封锁状态

 二 Winform 中使用的 thread

 首先可以看看最直接的方法,也是.net 1.0 下支持的方法。但请注意的是,此方法在.net 2.0以后就已经是一种错误的方法了。

 [csharp] view plain copy 在 CODE 上查看代码片派生到我的代码片 public partial class Form1 : Form

 {

  public Form1()

  {

  InitializeComponent();

  }

  private void Form1_Load(object sender, EventArgs e)

  {

  Thread thread = new Thread(ThreadFuntion);

  thread.IsBackground = true;

  thread.Start();

  }

  private void ThreadFuntion()

  {

  while (true)

  {

  this.textBox1.Text = DateTime.Now.ToString();

  Thread.Sleep(1000);

  }

  }

  }

  这段 code 在 vs2005 或者 2008 上都抛出异常 :Cross-thread operation not valid:Control "textBox1" accessed from a thread other than the thread it was created on . 这是因为.net 2.0 以后加强了安全机制,不允许在 winform 中直接跨线程访问控件的属性。那么怎么解决这个问题呢,下面提供几种方案。

 第一种方案:

 在 Thread 创建之气,将 Control.CheckForIllegalCrossThreadCalls 设为 false。

 此代码告诉编译器:在这个类中我们不检查跨线程的调用是否合法(如果没有加这句话运行也没有异常,那么说明系统以及默认的采用了不检查的方式)。然而,这种方法不可取。我们查看 CheckForIllegalCrossThreadCalls 这个属性的定义,就会发现它是一个 static 的,也就是说无论我们在项目的什么地方修改了这个值,他就会在全局起作用。而且像这种跨线程访问是否存在异常,我们通常都会去检查。如果项目中其他人修改了这个属性,那么我们的方案就失败了,我们要采取另外的方案。

 第二种方案

  [csharp] view plain copy 在 CODE 上查看代码片派生到我的代码片 namespace TestInvoker

  {

  public partial class Form1 : Form

  {

  public Form1()

  {

  InitializeComponent();

 }

  private void button1_Click(object sender, EventArgs e)

  {

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

  thread.IsBackground = true;

  thread.Start();

  //StartSomeWorkFromUIThread();

  //label1.Text = "Set value through another thread!";

  }

  private void StartSomeWorkFromUIThread()

  {

  if (this.InvokeRequired)

  {

  BeginInvoke(new EventHandler(RunsOnWorkerThread), null);

  }

  else

  {

  RunsOnWorkerThread(this, null);

  }

  }

  private void RunsOnWorkerThread(object sender, EventArgs e)

  {

  Thread.Sleep(2000);

  label1.Text = System.DateTime.Now.ToString();

  }

  }

  }

  通过上叙代码,可以看到问题已经被解决了,通过等待异步,我们就不会总是持有主线程的控制,这样就可以在不发生跨线程调用异常的情况下完成多线程对 winform 多线程控件的控制了。

 二 二 ThreadPool 与 Timer 本文接上文,继续探讨 WinForm 中的多线程问题,再次主要探讨 threadpool 和 timer。

 一 、ThreadPool

 线程池(ThreadPool)是一种相对较简单的方法,它适应于一些需要多个线程而又较短任务(如一些常处于阻塞状态的线程),它的缺点是对创建的线程不能加以控制,也不能设置其优先级。由于每个进程只有一个线程池,当然每个应用程序域也只有一个线程池(对线),所 以 你 将 发 现 ThreadPool 类 的 成 员 函 数 都 为 static !

 当 你 首 次 调 用ThreadPool.QueueUserWorkItem、 ThreadPool.RegisterWaitForSingleObject 等,便会创建线程池实例。下面我就线程池当中的两函数作一介绍:

 [csharp] view plain copy 在 CODE 上查看代码片派生到我的代码片 public static bool QueueUserWorkItem( //调用成功则返回 true

 WaitCallback callBack,//要创建的线程调用的委托

 object state //传递给委托的参数

 )//它的另一个重载函数类似,只是委托不带参数而已

  此函数的作用是把要创建的线程排队到线程池,当线程池的可用线程数不为零时(线程池有创建线程数的限制,缺身值为 25),便创建此线程,否则就排队到线程池等到它有可用的线程时才创建。

 [csharp] view plain copy 在 CODE 上查看代码片派生到我的代码片 public static RegisteredWaitHandle RegisterWaitForSingleObject(

 WaitHandle waitObject,// 要注册的 WaitHandle

 WaitOrTimerCallback callBack,// 线程调用的委托

 object state,//传递给委托的参数

 int TimeOut,//超时,单位为毫秒,

 bool executeOnlyOnce //是否只执行一次

 );

 public delegate void WaitOrTimerCallback(

 object state,//也即传递给委托的参数

 bool timedOut//true 表示由于超时调用,反之则因为 waitObject

 );

  此函数的作用是创建一个等待线程,一旦调用此函数便创建此线程,在参数 waitObject 变为终止状态或所设定的时间 TimeOut 到了之前,它都处于 “阻塞”状态,值得注意的一点是此“阻塞”与 Thread 的 WaitSleepJoin 状态有很大的不同:当某 Thread 处于 WaitSleepJoin状态时 CPU 会定期的唤醒它以轮询更新状态信息,然后再次进入 WaitSleepJoin 状态,线程的切换可是很费资源的;而用此函数创建的线程则不同,在触发它运行之前,CPU 不会切换到此线程,它既不占用 CPU 的时间又不浪费线程切换时间,但 CPU 又如何知道何时运行它?实际上线程池会生成一些辅助线程用来监视这些触发条件,一旦达到条件便启动相应的线程,当然这些辅助线程本身也占用时间,但是如果你需创建较多的等待线程时,使用线程池的优势就越加明显。

 更详细内容 demo:

 [csharp] view plain copy 在 CODE 上查看代码片派生到我的代码片 namespace TestMethodInvoker

  {

  public partial class Form2 : Form

  {

  public Form2()

  {

  InitializeComponent();

  }

  private void button1_Click(object sender, EventArgs e)

  {

 //ThreadPool.RegisterWaitForSingleObject(

  //

 ev,

  //

 new WaitOrTimerCallback(WaitThreadFunc),

  //

 4,

  //

 2000,

  //

 false//表示每次完成等待操作后都重置计时器,直到注销等待

 //

 );

  ThreadPool.QueueUserWorkItem(new WaitCallback(ThreadFunc), "test1");

  //Thread.Sleep(10000);

  }

  private delegate void MyInvokeDelegate(string name);

  private void Test(object o)

  {

  richTextBox1.Text += string.Format("the object is {0} \n", o);

  }

  public

 void ThreadFunc(object b)

  {

  this.Invoke(new MyInvokeDelegate(Test), b);

  }

  public void WaitThreadFunc(object b, bool t)

  {

  richTextBox1.Text += string.Format("the object is {0},t is {1}\n", b, t);

  }

 }

  }

 一个很值得扩展的地方时,这里的 invoke 用的是代理,其实还有其他的方法,比如 action 和 func。实例代码如下:

 [csharp] view plain copy 在 CODE 上查看代码片派生到我的代码片 this.Invoke(new Action<string>(this.ChangeText), o.ToString());

  this.Invoke(new Action(delegate() { this.textBox1.Text = o.ToString();}));

  private void DoSomething(object o)

 {

  System.Func<string, int> f = new Func<string, int>(this.GetId);

  object result = this.Invwww.shanxiwang.netoke(f, o.ToString());

  MessageBox.Show(result.ToString());

  }

  private int GetId(string name)

 {

  this.textBox1.Text = name;

  if (name == "Y")

 {

 return 999;

  }

 else

 {

  return 0;

  }

  }

  二、 Timer

 它适用于需周期性调用的方法,它不在创建计时器的线程中运行,它在由系统自动分配的单独线程中运行。这和 Win32 中的 SetTimer 方法类似。它的构造为:

 [csharp] view plain copy 在 CODE 上查看代码片派生到我的代码片 public Timer(

 TimerCallback callback,//所需调用的方法

 object state,//传递给 callback 的参数

 int dueTime,//多久后开始调用 callback

 int period//调用此方法的时间间隔

 );//

  如果 dueTime 为 0,则 callback 立即执行它的首次调用。如果 dueTime 为 Infinite,则 callback 不调用它的方法。计时器被禁用,但使用 Change 方法可以重新启用它。如果 period 为 0 或 Infinite,并且 dueTime 不为 Infinite,则 callback 调用它的方法一次。计时器的定期行为被禁用,但使用 Change 方法可以重新启用它。如果 period 为零 (0) 或 Infinite,并且 dueTime 不为 Infinite,则 callback 调用它的方法一次。计时器的定期行为被禁用,但使用 Change 方法可以重新启用它。

  在创建计时器之后若想改变它的 period 和 dueTime,我们可以通过调用 Timer 的 Change 方法来改变:

 [csharp] view plain copy 在 CODE 上查看代码片派生到我的代码片 public bool Change(

 int dueTime,

 int period

 );//

  显然所改变的两个参数对应于 Timer 中的两参数。

 三 三 Control.Invoke 下面我们就把在 Windows Form 软件中使用 Invoke 时的多线程要注意的问题给大家做一个介绍。

 首先,什么样的操作需要考虑使用多线程?总的一条就是,负责与用户交互的线程(以下简称为 UI 线程)应该保持顺畅,当 UI 线程调用的 API 可能引起阻塞时间超过 30 毫秒时(比如访问 CD-ROM 等速度超慢的外设、进行远程调用等等)就应该考虑使用多线程。为什么是30 毫秒?30 毫秒的概念是人眼可以察觉到的一个迟滞,大约等同于电影里的一帧停留的时间,最长不要超过 100 毫秒。

  第二,最方便和简单的多线程是使用线程池。通过线程池里的线程运行代码的最简便方法则是使用异步委托调用。注意委托调用通常是同步完成的,请使用 BeginInvoke 方法,这样就可以把要调用的方法排队到线程池里等候处理,而程序的流程会立刻返回到调用方(此处是 UI 线程),而调用方因此不会出现阻塞。

 看看下面的例子我们就发现要使用线程池异步执行代码也并非十分复杂,这里我们利用 System.Windows.Forms.MethodInvoker 委托进行异步调用。注意 MethodInvoker 委托不接受方法参数,如果需要向异步执行的方法传递参数,请使用其他委托,或者需要自己定义。

 [csharp] view plain copy 在 CODE 上查看代码片派生到我的代码片 private void StartSomeWorkFromUIThread () {

  // 我们要做的工作相对 UI 线程而言台慢了,用下面的方法异步进行处理

  MethodInvoker mi = new MethodInvoker(RunsOnWorkerThread);//这是入口方法

  mi.BeginInvoke(null, null); // 这样就不会阻塞

  }

 // 缓慢的工作在此方法内进行处理,使用线程池里的线程

  private void RunsOnWorkerThread() {

  DoSomethingSlow();

  }

  归纳上述方法,对 UI 线程而言实际上就是:1、发出调用,2、立刻返回,具体运行过程不理了,这样 UI 线程就不会被阻塞。这种方法很重要,下面我们会深入介绍。除了上面的方法,还有其他使用线程池的方法,当然如果你高兴也可以自己创建线程。

 第三,在 Windows Form 中使用多线程的,最重要的一条注意事项是,除了创建控件的线程以外,绝对不要在任何其他线程里面调用控件的成员(只有极个别情况例外),也就是说控 件 属 于 创 建 它 的 线 程 , 不 能 从 其 他 线 程 里 面 访 问 。

 这 一 条 适 用 于 所 有 从System.Windows.Forms.Control 派生的控件(因此可以说是几乎所有控件),包括 Form 控件本身也是。举一反三,我们很容易得出这样的结论,控件的子控件必须由创建控件的线程来创建,比如一个表单上的按钮,比如由创建表单的线程来创建,因此,一个窗口中的所有控件实际上都活在同一个线程之中。在实际编程时,大多数的软件的做法都是让同一线程负责全部的控件,这就是我们所说的 UI 线程。看下面的例子:

 [csharp] view plain copy 在 CODE 上查看代码片派生到我的代码片 // 这是由 UI 线程定义的 Label 控件

  private Label lblStatus;

  // 以下方法不在 UI 线程上执行

  private void RunsOnWorkerThread() {

  DoSomethingSlow();

  lblStatus.Text = "Finished!";

 // 这是错的

  }

  我们要特别提醒大家,很多人刚开始的时候都会使用以上的方法来访问不在同一个线程里的控件(包括笔者本人),而且在 1.0 版.Net 框架上似乎没有发现问题,但是这根本就是

 错的,更糟糕的是,程序员在这里不会得到任何错误提示,一开始就上当受骗,之后会莫明其妙地发现其他错误,这就是 Windows Form 多线程编程的痛苦所在。笔者试过花很多时间来 Debug 自己写的 Splash 窗口突然消失的问题,结果还是失败了:笔者在软件的引导过程中,用另外一个线程里创建了一个 Splash 窗口来显示欢迎信息,然后尝试把主线程里引导的状态直接写入到 Splash 窗口上的控件中,开始还 OK,可是过一会 Splash 窗口就莫明其妙消失了。

 理解了这一点,我们应该留意到,有时候即使没有用 System.Threading.Thread 来显式创建一个线程,我们也可能因为使用了异步委托的 BeginInvoke 方法来隐式创建了线程(从线程池里),在这种线程里也同样不能调用 UI 线程所创建的控件的成员。

 第四,由于上述限制,我们可能会感到很不方便,的确,当我们利用一个新创建的线程来执行某些花时间的运算时,怎样知道运算进度如何并通过 UI 反映给用户呢?解决方法很多!比如熟悉多线程编程的用户很快会想到,我们采用一些低级的同步方法,工作者线程把状态保存到一个同步对象中,让 UI 线程轮询(Polling)该对象并反馈给用户就可以了。不过,这还是挺麻烦的,实际上不用这样做,Control 类(及其派生类)对象有一个 Invoke 方法很特别,这是少数几个不受线程限制的成员之一。我们前面说到,绝对不要在任何其他线程里面调用非本线程创建的控件的成员时,也说了“只有极个别情况例外”,这个 Invoke 方法就是极个别情况之一----Invoke 方法可以从任何线程里面调用。下面我们来讲解 Invoke 方法。

 Invoke 方法的参数很简单,一个委托,一个参数表(可选),而 Invoke 方法的主要功能就是帮助你在 UI 线程(即创建控件的线程)上调用委托所指定的方法。Invoke 方法首先检查发出调用的线程(即当前线程)是不是 UI 线程,如果是,直接执行委托指向的方法,如果不是,它将切换到 UI 线程,然后执行委托指向的方法。不管当前线程是不是 UI 线程,Invoke 都阻塞直到委托指向的方法执行完毕,然后切换回发出调用的线程(如果需要的话),返回。注意,使用 Invoke 方法时,UI 线程不能处于阻塞状态。以下 MSDN 里关于 Invoke 方法的说明:

 [plain] view plain copy 在 CODE 上查看代码片派生到我的代码片 “控件上有四种方法可以安全地从任何线程进行调用:Invoke、BeginInvoke、EndInvoke 和 CreateGraphics。对于所有其他方法调用,则应使用调用 (invoke) 方法之一封送对控件的线程的调用。

  委托可以是 EventHandler 的实例,在此情况下,发送方参数将包含此控件,而事件参数将包含 EventArgs.Empty。委托还可以是 MethodInvoker 的实例或采用 void 参数列表的其他任何委托。调用 EventHandler 或 MethodInvoker 委托比调用其他类型的委托速度更快。”

  好了,说完 Invoke,顺便说说 BeginInvoke,毫无疑问这是 Invoke 的异步版本(Invoke是同步完成的),不过大家不要和上面的 System.Windows.Forms.MethodInvoker 委托中的BeginInvoke 混淆,两者都是利用不同线程来完成工作,但是控件的 BeginInvoke 方法总是使用 UI 线程,而其他的异步委托调用方法则是利用线程池里的线程。相对 Invoke 而言,使用 BeginInvoke 稍稍麻烦一点,但还是那句话,异步比同步效果好,尽管复杂些。比如同步方法可能出现这样一种死锁情况:工作者线程通过 Invoke 同步调用 UI 线程里的方法时会阻塞,而万一 UI 线程正在等待工作者线程做某件事时怎么办?因此,能够使用异步方法时应尽量使用异步方法。

  下面我们利用所学到的知识来改写上面那个简单的例子:

 [csharp] view plain copy 在 CODE 上查看代码片派生到我的代码片 // 这是由 UI 线程定义的 Label 控件

  private Label lblStatus;

  // 以下方法不在 UI 线程上执行

  private void RunsOnWorkerThread() {

  DoSomethingSlow();

  // Do UI update on UI thread

  object[] pList = { this, System.EventArgs.Empty };

  lblStatus.BeginInvoke(

  new System.EventHandler(UpdateUI), pList);

  }

  // 切换回 UI 线程执行的入口

  private void UpdateUI(object o, System.EventArgs e) {

  //现在没问题了,使用 Invoke 使得线程总是回到 UI 线程,所以我们可以放心大胆地调用控件的成员了

  lblStatus.Text = "Finished!";

  }

  第五,关于多线程编程还要考虑线程之间的同步问题、死锁和争用条件,有关这类问题的文章很多,我们就不赘述了

相关热词搜索: 多线程 开发 CWinForm