加载中…
个人资料
  • 博客等级:
  • 博客积分:
  • 博客访问:
  • 关注人气:
  • 获赠金笔:0支
  • 赠出金笔:0支
  • 荣誉徽章:
正文 字体大小:

.NET 线程的详解

(2014-11-25 09:08:39)
标签:

股票

分类: 程序世界

一概论

多线程在构建大型系统的时候是需要重点关注的一个重要方面,特别是在效率(系统跑得多快?)和性能(系统工作正常?)之间做一个权衡的时候。恰当的使用多线程可以极大的提高系统性能。

 什么是线程?
  每个正在系统上运行的程序都是一个进程。每个进程包含一到多个线程。进程也可能是整个程序或者是部分程序的动态执行。线程是一组指令的集合,或者是程序的特殊段,它可以在程序里独立执行。也可以把它理解为代码运行的上下文。所以线程基本上是轻量级的进程,它负责在单个程序里执行多任务。通常由操作系统负责多个线程的调度和执行。



多线程的好处:在于可以提高CPU的利用率

不利方面:

  1. 线程也是程序,所以线程需要占用内存,线程越多占用内存也越多
  2. 多线程需要协调和管理,所以需要CPU时间跟踪线程
  3.  线程之间对共享资源的访问会相互影响,必须解决竞用共享资源的问题
  4.  线程太多会导致控制太复杂,最终可能造成很多Bug


以一个公司来比喻线程:

有利方面:

    在公司里,你可以一个职员干所有的事,但是效率很显然是高不起来的,一个人的公司也不可能做大,然而各司其职的员工无疑使公司效率更高

不利方面:

    公司的职员越多,老板就得发越多的薪水给他们,还得耗费大量精力去管理他们,协调他们之间的矛盾和利益


说了那么多,现在我们来讨论一下在Win32环境中常用的一些模型。

(掌握级别:了解)

  ·单线程模型

  在这种线程模型中,一个进程中只能有一个线程,剩下的进程必须等待当前的线程执行完。这种模型的缺点在于系统完成一个很小的任务都必须占用很长的时间。

  ·块线程模型(单线程多块模型STA

  这种模型里,一个程序里可能会包含多个执行的线程。在这里,每个线程被分为进程里一个单独的块。每个进程可以含有多个块,可以共享多个块中的数据。程序规定了每个块中线程的执行时间。所有的请求通过Windows消 息队列进行串行化,这样保证了每个时刻只能访问一个块,因而只有一个单独的进程可以在某一个时刻得到执行。这种模型比单线程模型的好处在于,可以响应同一 时刻的多个用户请求的任务而不只是单个用户请求。但它的性能还不是很好,因为它使用了串行化的线程模型,任务是一个接一个得到执行的。(此模型相当于在多 线程中在一个线程执行的过程中,另外的线程都处于挂起的状态,直到此线程执行结束,后再开始另一个线程)

  ·多线程块模型(自由线程块模型)

  多线程块模型(MTA)在每个进程里只有一个块而不是多个块。这单个块控制着多个线程而不是单个线程。这里不需要消息队列,因为所有的线程都是相同的块的一个部分,并且可以共享。这样的程序比单线程模型和STA的执行速度都要块,因为降低了系统的负载,因而可以优化来减少系统idle的时间。这些应用程序一般比较复杂,因为程序员必须提供线程同步以保证线程不会并发的请求相同的资源,因而导致竞争情况的发生。这里有必要提供一个锁机制。但是这样也许会导致系统死锁的发生。

多线程在.NET里如何工作?

(掌握级别:了解)

在本质上和结构来说,.NET是一个多线程的环境。有两种主要的多线程方法是.NET所提倡的:使用ThreadStart来开始你自己的进程,直接的(使用ThreadPool.QueueUserWorkItem)或者间接的(比如Stream.BeginRead,或者调用BeginInvoke)使用ThreadPool类。一般来说,你可以"手动"为 长时间运行的任务创建一个新的线程,另外对于短时间运行的任务尤其是经常需要开始的那些,进程池是一个非常好的选择。进程池可以同时运行多个任务,还可以 使用框架类。对于资源紧缺需要进行同步的情况来说,它可以限制某一时刻只允许一个线程访问资源。这种情况可以视为给线程实现了锁机制。线程的基类是System.Threading。所有线程通过CLI来进行管理。


二操纵一个线程

下面通过一些实例解决对线程的控制,多线程间通讯等问题

任何程序在执行时,至少有一个主线程,下面这段小程序可以给读者一个直观的印象:
  [CODE]
  //SystemThread.cs
  using System;
  using System.Threading;
  
  namespace ThreadTest
  {
  
  class RunIt
  
  {
  
    [STAThread]
  
    static void Main(string[] args)
  
    {
  
      Thread.CurrentThread.Name="System Thread";
 Console.WriteLine(Thread.CurrentThread.Name+"'Status:"+Threa d.CurrentThread.ThreadState); //
当前状态(ThreadState
  
      Console.ReadLine();
  
    }
  
  }
  }
  [/CODE]
  
编译执行后你看到了什么?是的,程序将产生如下输出:
  
  System Thread's Status:Running

CurrentThread Thread类的静态属性

所谓静态属性,就是这个类所有对象所公有的属性,不管你创建了多少个这个类的实例,但是类的静态属性在内存中只有一个。很容易理解CurrentThread为什么是静态的——虽然有多个线程同时存在,但是在某一个时刻,CPU只能执行其中一个。


现在注意到程序的头部

 using System.Threading;
所有与多线程机制应用相关的类都是放在System.Threading命名空间中的

Thread用于创建线程

ThreadPool用于管理线程池等等

如果你想在你的应用程序中使用多线程,就必须包含这个Thread

Thread类有几个至关重要的方法:

 Start():启动线程
 Sleep(int):
静态方法,暂停当前线程指定的毫秒数
 Abort():
通常使用该方法来终止一个线程
 Suspend()
:该方法并不终止未完成的线程,它仅仅挂起线程,以后还可恢复。
 Resume():
恢复被Suspend()方法挂起的线程的执行


下面我们来看一个两个线程的例子

(在下面的例子中有一个明显的线程争夺资源的问题)

namespace Programming_CSharp
{
using System;
using System.Threading;
class Test
{
static void Main( )
{
// 生成一个本类的实例
Test new Test( );
t.DoTest( );
}
public void DoTest( )
{
//Incrementer开一个线程并传入ThreadStart代理
Thread t1 =new Thread(new ThreadStart(Incrementer) );
//Decrementer开一个线程并传入ThreadStart代理
Thread t2 =new Thread(new ThreadStart(Decrementer) );
// 线程开始
t1.Start( );
t2.Start( );
}
public void Incrementer( )
{
for (int =0;i<1000;i++)
{
Console.WriteLine("
增加{0}", i);
}
}
public void Decrementer( )
{
for (int 1000;i>=0;i--)
{
Console.WriteLine("
减少{0}", i);
}
}
}
}


       
输出:

增加102
增加103
增加104
增加105
增加106
减少1000
减少999
减少998
减少997
注意:输出为两个线程交替执行的结果(资源争夺)


阻塞线程:阻塞调用线程,直到某个线程终止时为止

例:t2.Join( )

暂停线程:

例:t2.Sleep(毫秒数)


挂起线程:

例:t2. Suspend()---------[恢复挂起的线程 例:t2. Resume()]


线程的高级使用__异步调用

例:

namespace Programming_CSharp
{
using System;
using System.Threading;
class Test
{
private int counter 0;
static void Main( )
{
//生成一个本类的实例
Test new Test ( );
t.DoTest( );
}
public void DoTest( )
{
Thread t1 
new Thread( new ThreadStart(Incrementer) );
t1.IsBackground=
true;
t1.Name "
线程1";
t1.Start( );
Console.WriteLine("
开始线程 {0}",
t1.Name);
Thread t2 
new Thread( new ThreadStart(Incrementer) );
t2.IsBackground=
true;
t2.Name "ThreadTwo";
t2.Start( );
Console.WriteLine("
开始线程 {0}",
t2.Name);
t1.Join( );
t2.Join( );

Console.WriteLine("
所有线程完成.");
}



public void Incrementer( )
{
try
{
while (counter 1000)
{
int temp counter;
temp++; 
// 增加
Thread.Sleep(1);
counter temp;
Console.WriteLine("
线程 {0}. 增加{1}",Thread.CurrentThread.Name,counter);
}
}
catch (ThreadInterruptedException)
{
Console.WriteLine("
线程 {0} 中断清空中…",Thread.CurrentThread.Name);
}
finally
{
Console.WriteLine("
线程 {0} 退出",Thread.CurrentThread.Name);
}
}
}
}


输出:


线程开始 线程1
线程开始 线程2
线程 线程1. 增加1
线程 线程2. 增加2
线程 线程1. 增加3
线程 线程2. 增加4
线程 线程1. 增加5
线程 线程2. 增加6
线程 线程1. 增加7
线程 线程2. 增加8
线程 线程1. 增加9
线程 线程2. 增加10
线程 线程增加11
线程 线程2. 增加12
线程 线程1. 增加13
线程 线程2. 增加14
线程 线程1. 增加15
线程 线程2. 增加16
线程 线程1. 增加17
线程 线程2. 增加18
刚才我们看了一个异步调用的例子,那现在我们就来对异步调用进行些探讨。

一:

首先:

    我们先对异步调用同步调用进行个比较:

    1.在单线程方式下,计算机是一台严格意义上的冯·诺依曼式机器,一段代码调用另一段代码时,只能采用同步调用,必须等待这段代码执行完返回结果后,调用方才能继续往下执行。

2. 在多线程方式下,可以采用异步调用,调用方和被调方可以属于两个不同的线程,调用方启动被调方线程后,不等对方返回结果就继续执行后续代码。被调方执行完毕后,通过某种手段通知调用方

另外,异步调用用来处理从外部输入的数据特别有效


例:

某个程序启动后如果需要打开文件读出其中的数据,再根据这些数据进行一系列初始 化处理,程序主窗口将迟迟不能显示,让用户感到这个程序怎么等半天也不出来,太差劲了。借助异步调用可以把问题轻松化解:把整个初始化处理放进一个单独线 程,主线程启动此线程后接着往下走,让主窗口瞬间显示出来。等用户盯着窗口犯呆时,初始化处理就在背后悄悄完成了。程序开始稳定运行以后,还可以继续使用 这种技巧改善人机交互的瞬时反应。用户点击鼠标时,所激发的操作如果较费时,再点击鼠标将不会立即反应,整个程序显得很沉重。借助异步调用处理费时的操 作,让主线程随时恭候下一条消息,用户点击鼠标时感到轻松快捷,肯定会对软件产生好感。

二:

在上面我们提到:"被调方执行完毕后,通过某种手段通知调用方",下面我们就来看看可以采取哪些手段来通知调用方

在同一进程中有很多手段可以利用,常用的手段有1.回调;2.event 对象; 3.消息。

  1. 回调: 调用异步函数时在参数中放入一个函数地址,异步函数保存此地址,待有了结果后回调此函数便可以向调用方发出通知。如果把异步函数包装进一个对象中,可以用事件取代回调函数地址,通过事件处理例程向调用方发通知。


示意图见下一页





  1. event 对象:event 是 Windows 系统提供的一个常用同步对象,以在异步处理中对齐不同线程之间的步点。如果调用方暂时无事可做,可以调用 wait 函数等在那里,此时 event 处于 nonsignaled 状态。当被调方出来结果之后,把 event 对象置于 signaled 状态,wait 函数便自动结束等待,使调用方重新动作起来,从被调方取出处理结果。这种方式比回调方式要复杂一些,速度也相对较慢,但有很大的灵活性,可以搞出很多花样 以适应比较复杂的处理系统。
  2. 消息:   借助 Windows 消息发通知是个不错的选择,既简单又安全。程序中定义一个用户消息,并由调用方准备好消息处理例程。被调方出来结果之后立即向调用方发送此消息,并通过 WParam 和 LParam 这两个参数传送结果。消息总是与窗口 handle 关联,因此调用方必须借助一个窗口才能接收消息,这是其不方便之处。另外,通过消息联络会影响速度,需要高速处理时回调方式更有优势。

另外:如果调用方和被调方分属两个不同的进程, 由于内存空间的隔阂,一般是采用 Windows 消息发通知比较简单可靠,被调方可以借助消息本身向调用方传送数据。event 对象也可以通过名称在不同进程间共享,但只能发通知,本身无法传送数据,需要借助 Windows 消息和 FileMapping 等内存共享手段或借助  MailSlot 和 Pipe 等通信手段。


提醒:异步调用原理并不复杂,但实际使用时容易出莫名其妙的问题,特别是不同线程共享代码或共享数据时容易出问题,编程时需要时时注意是否存在这样的共享,并通过各种状态标志避免冲突。Windows 系统提供的 mutex 对象用在这里特别方便。mutex 同一时刻只能有一个管辖者。 一个线程放弃管辖权后,另一线程才能接管。当某线程执行到敏感区之前先接管 mutex,使其他线程被 wait 函数堵在身后;脱离敏感区之后立即放弃管辖权,使 wait 函数结束等待,另一个线程便有机会光临此敏感区。这样就可以有效避免多个线程进入同一敏感区(异步调用容易出问题,要设计一个安全高效的编程方案需要比较 多的设计经验,所以最好不要滥用异步调用)。


线程安全

    在 前面我举了一个抛球的例子,这里我们再来看看,想象一下如果每次我们都是抛三个球,让在空中的那个球悬停在原处,这时将左手的球抛向右手,好,这是让空中 的那个球落下同时右手的球抛向空中,左手的球抛向右手,此时又让空中的球停住,这个时候,之前停在空中的球已经顺利到了你的左手。依次这般,抛球就会变的 非常简单。这就是线程安全。在我们的程序之中强行的让一个线程等待另一个线程完成后再开始,这种情况就叫做线程阻塞或是线程异步。在C# 中我们会锁住内存的一部分(通常是一个对象的实例)不允许其他线程进入,直到调用这部分内存的线程结束时才可以。好了,说了那么多,还是让我们来看看一些例子吧

在下面的例子中,将会创建两个线程,Thread 1 Thread 2一个共享变量threadOurput.threadOutput 将会根据他所在的线程而被赋予不同的消息。下面我们就来看看。

1

using System;

using System.Collections.Generic;

using System.Text;

using System.Threading;


namespace ConsoleApplication1

{

class Program

{


bool stopThreads = false;


private string threadOutput = "";



void DisplayThread1()

{


while (stopThreads == false)

{


Console.WriteLine("显示线程");

threadOutput = "好啊,线程1";

Thread.Sleep(1000);

Console.WriteLine("线程输出--> {0}", threadOutput);

}


}

void DisplayThread2()

{


while (stopThreads == false)

{

Console.WriteLine("显示线程");

threadOutput = "好啊,线程2";

Thread.Sleep(1000);

Console.WriteLine("线程输出--> {0}", threadOutput);

}


}

void class1()

{

Thread thread1 = new Thread(new ThreadStart(this.DisplayThread1));

Thread thread2 = new Thread(new ThreadStart(this.DisplayThread2));

thread1.Start();

thread2.Start();

}

static void Main(string[] args)

{

Program program = new Program();

program.class1();

}


}


}

上面代码的输出结果显示在图2中。仔细地看这个结果,你会发现这段程序给出了与预料的不一致的结果。虽然我们认真地给threadOutput赋予了相应的字符串,但是输出依然不是我们所预期的那样,这是为什么呢?


2 –两个不正常的输出.



解释,为什么?

以本程序为例,看到这样结果的原因是代码执行了两个方法DisplayThread1DisplayThread2,每个方法都使用变量threadOutput , 所以很有可能threadOutput虽然在线程1中被赋上了值"好啊,线程1"并且把它显示出来,但是在线程1threadOutput赋值并将其值显示的时候,这时线程2threadOutput的值赋为"好啊,线程2"。由于对同一变量的占用,出现如图1那样的奇怪结果也就完全可能了。这个很是令人头疼的线程问题在线程的编程中相当平常,通常被叫做争用条件。对于程序而言,在某些时候争用简直就是噩梦,因为它不常出现而且很难很难去再现。

在争用中获胜

 避免争用的最好方式是编写线程安全的代码。 如果你的代码是线程安全的,就能防止一些突发的线程问题。 下面是一些编写线程安全代码的技巧。首先是尽量少的去共享内存。如果你创建了一个类的实例而且他运行在线程中,而此时你又在另一个线程中创建相同类的实例,这些类是线程安全的,只要他们没有包含任何静态变量。这两个类在内存中都有自己的运行域,没有共享内存 。如果在类或实例中确实含有静态变量并对其他线程共享,你就必须要寻找到某个方式确保一个线程不能使用这部分内存知道其他的某一线程完成对其的使用。 我们管这种防止其他线程影响被占用内存的方法叫做锁定C#中允许我们上锁代码通过Monitor类或lock{}结构 。对lock的使用,让我们看看下面这段代码吧。


例 2:

using System;

using System.Collections.Generic;

using System.Text;

using System.Threading;


namespace ConsoleApplication1

{

class Program

{

bool stopThreads = false;

private string threadOutput = "";

void DisplayThread1()

{

while (stopThreads == false)

{

lock (this)

{

Console.WriteLine("显示线程1");

threadOutput = "好啊,线程1";

Thread.Sleep(1000);

Console.WriteLine("线程输出--> {0}", threadOutput);

}

}

}

void DisplayThread2()

{

while (stopThreads == false)

{

lock (this)

{

Console.WriteLine("显示线程2");

threadOutput = "好啊,线程2";

Thread.Sleep(1000);

Console.WriteLine("线程输出--> {0}", threadOutput);

}


}


}

void class1()

{

Thread thread1 = new Thread(new ThreadStart(this.DisplayThread1));

Thread thread2 = new Thread(new ThreadStart(this.DisplayThread2));

thread1.Start();

thread2.Start();

}

static void Main(string[] args)

{

Program program = new Program();

program.class1();

}


}

}

对线程上锁后的运行结果如图3所示 注意他们都同步正常显示了,正如我们所需要的结果一样

因为谁也不想让线程1去等线程2的锁释放,让线程2去等待线程1的锁释放,你等我,我又等你,勇无终日, 造成了死锁。这是谁也不希望发生的。


 2 –对双线程上锁的异步调用

小结:对多线程的使用,是一个较为高级的技术,然而他又是一把双刃剑,使用得好,可以有效的提高性能,反之,则会使性能下降,甚至造成死机。线程还有很多的应用





    


上面我们讲了很多的总体的东西,现在我们就来看看具体的设计

浅析.Net下的多线程编程

下面介绍在.Net下进行多线程编程的基本方法和步骤:

开始新线程

:
Thread thread1 = new Thread1 (new ThreadStart (ThreadFunc));
thread1.Start ();
第一条语句创建一个新的Thread对象,并指明了一个该线程的方法ThreadFunc()

第二条语句正式开始该新线程,一旦方法Start()被调用,该线程就保持在一个"alive"的状态下了,你可以通过读取它的IsAlive属性来判断它是否处于"alive"状态。下面的语句显示了如果一个线程处于"alive"状态下就将该线程挂起的方法:
if (thread.IsAlive)

{
thread.Suspend ();

}

    不过请注意,线程对象的Start()方法只是启动了该线程,而并不保证其线程方法ThreadFunc()能立即得到执行。它只是保证该线程对象能被分配到CPU时间,而实际的执行还要由操作系统根据处理器时间来决定。

杀死线程
  Thread类的Abort方法用于永久地杀死一个线程。但是请注意,在调用Abort方法前一定要判断线程是否还激活,:
if ( mythread.IsAlive )
{
  mythread.Abort();
}

暂停线程
  Thread.Sleep方法用于将一个线程暂停一段时间,代码如下:
  mythread.Sleep(int);

设置线程的优先权
  我们可以使用Thread类的ThreadPriority属性设置线程的优先权。线程优先权的取值范围是NormalAboveNormalBelowNormalHighest或者Lowest。请看下面的设置代码:  

mythread.Priority = ThreadPriority.Highest;

延迟线程
Thread
类的Suspend方法可以延迟一个线程(挂起线程)。线程被延迟到调用Resume方法为止。

if (mythread.ThreadState = ThreadState.Running )
{
  mythread.Suspend();
}

恢复被延迟的线程
  调用Resume方法可以恢复一个被延迟的线程。如果线程没有被延迟,Resume方法就是无效的。
if (mythread.ThreadState = ThreadState.Suspended )
{
  mythread.Resume();
}


现在研究一下线程方法

一个线程的方法不包含任何参数,同时也不返回任何值。它的命名规则和一般函数的命名规则相同。它既可以是静态的(static)也可以是非静态的(nonstatic)。当它执行完毕后,相应的线程也就结束了,其线程对象的IsAlive属性也就被置为false了。下面是一个线程方法的实例:

public static void ThreadFunc( )
{ for (int i = 0; i <10; i++) {
Console.WriteLine("ThreadFunc {0}", i); } }  

现在来讨论一下前台线程和后台线程

区别:应用程序必须运行完所有的前台线程才可以退出;而对于后台线程,应用程序则可以不考虑其是否已经运行完毕而直接退出,所有的后台线程在应用程序退出时都会自动结束。

关于一个线程是否是前台线程:一个线程是前台线程还是后台线程可由它的IsBackground属性来决定。这个属性是可读又可写的。它的默认值为false,即意味着一个线程默认为前台线程。我们可以将它的IsBackground属性设置为true,从而使之成为一个后台线程。

示例:

    下面的例子是一个控制台程序,程序一开始便启动了10个线程,每个线程运行5秒钟时间。由于线程的IsBackground属性默认为false,即它们都是前台线程,所以尽管程序的主线程很快就运行结束了,但程序要到所有已启动的线程都运行完毕才会结束。示例代码如下:

using System;

using System.Threading;

class MyApp

{

public static void Main()

{

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

{

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

thread.Start();

}

}

private static void ThreadFunc()

{

DateTime start = DateTime.Now;

while ((DateTime.Now-start).Seconds < 10);

}

}







接下来我们对上面的代码进行略微修改,将每个线程的IsBackground属性都设置为true,则每个线程都是后台线程了。那么只要程序的主线程结束了,整个程序也就结束了。

示例代码如下:

using System;

using System.Threading;

class MyApp

{

public static void Main()

{

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

{

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

thread.IsBackground = true;

thread.Start();

}

}

private static void ThreadFunc()

{

DateTime start = DateTime.Now;

while ((DateTime.Now - start).Seconds < 5);

}

}


问题:

    既然前台线程和后台线程有这种差别,那么我们怎么知道该如何设置一个线程的IsBackground属性呢?

解决问题:

    对于一些在后台运行的线程,当程序结束时这些线程没有必要继续运行了,那么这些线程就应该设置为后台线程。

    比如:一 个程序启动了一个进行大量运算的线程,可是只要程序一旦结束,那个线程就失去了继续存在的意义,那么那个线程就该是作为后台线程的。而对于一些服务于用户 界面的线程往往是要设置为前台线程的,因为即使程序的主线程结束了,其他的用户界面的线程很可能要继续存在来显示相关的信息,所以不能立即终止它们。这里 我只是给出了一些原则,具体到实际的运用往往需要实验室各编程人员的进一步仔细斟酌。


看了那么久在控制台实现的多线程,那么我们现在再在Windows平台上对多线程怎样在窗体程序中的使用进行一些探讨。下面我们就来看一个例子:

using System;

using System.Collections.Generic;

using System.ComponentModel;

using System.Text;

using System.Windows.Forms;

using System.Threading;

namespace WindowsApplication1

{

public partial class Form1 : Form

{

public Form1()

{

InitializeComponent();

}


private void Form1_Load(object sender, EventArgs e)

{

}

void ShowProgress(int totalStep, int currentStep)

{

Progress.Maximum = totalStep;

Progress.Value = currentStep;

}

void RunTask(int seconds)

{

for (int i = 0; i < seconds * 4; i++)

{

Thread.Sleep(250);

ShowProgress(seconds * 4, i + 1);

}

}

private void button1_Click(object sender, EventArgs e)

{

RunTask(Convert.ToInt32(txtSecond.Text));

}

private void button2_Click(object sender, EventArgs e)

{

this.Close();

}

}

}

种类用程序都需要长时间操作,比如:行一打印任求一 Web Service 用等。用这种下一般移做其他事情等待任的完成,同时还希望随时可以控任度。
下面就是上面程序的执行结果:

们运行上面的程序,在整个长程中,有出任何问题这样问题换应用程序去做其他事情后再切问题生了!主窗体就如下情



这个问题当会发生,因们现在的用程序是单线程的,因此,当线务时也就不能重界面了

在我换应用程序后,问题生呢这是因为当你切换当前应用程序到后台再切换回前台时,我们需要重画整个用户界面。但是应用程序正在执行长任务,根本没有时间处理用户界面的重画,问题就会发生。如何解决问题呢?我们需要将长任务放在后台运行,把用户界面线程解放出来,因此我们需要另外一个线程。


线步操作

上面程序中行按的Click 理如下:

private void button1_Click( object sender, System.EventArgs )
{
    RunTask( Convert.ToInt32( txtSecond.Value );
}

回想上面
问题发生的原因,直到 RunTask 行完成后返回,Click 理函不能返回,就意味着用界面不能理重事件或其他任何事件。一方法就是建另外一个线程,代如下:

using System.Threading;
private int seconds;
// 行任工作线入点
void RunTaskThreadStart()
{
    RunTask( seconds );
}
// 过创建工作线程消除用界面线程的阻塞问题
private void button1_Click( object sender, System.EventArgs )
{
    seconds Convert.ToInt32(txtSecond.Value );
    Thread runTaskThread 
new Thread( new ThreadStart( RunTaskThreadStart );        
    runTaskThread.Start();
}

现在,我们不再需要等待 RunTask 执行完成才能够从 Click 事件返回,我们创建了新的工作线程并让它开始工作、运行。


runTaskThread.Start()
方法建的工作线立即返回允许我们的用户界面线程重新获得控制权执行它自己的工作。现在如果用户再切换应用程序,因为工作线程在自己的空间执行长任务,用户界面线程被解放出来处理包括用户界面重画的各种事件,我们上面遇到的问题就解决了。

虽然上面的问题似乎是解决了,但也存在着问题:

    在 上面的代码中,我们注意到,我们没有给工作线程进入点( RunTaskThreadStart )传递任何参数,我们采用声明一个窗体类的字段 _seconds 来给工作线程传递参数。在某种应用场合不能够给工作线程直接传递参数也是一件非常痛苦的事情。(正是因为不能传递参数,所以创建了不带参数的 RunTaskThreadStart ()方法,反过来说,如果能传递参数,那么,就可以写成

Thread myThread = new Thread(new ThreadStart(RunTask(Convert.ToInt32(seconds)))

那么如何改进呢?

我们可以使用委托来进行异步调用。委托是支持传递参数的。这样,就消除了我们刚才的问题,使我们能够消除额外的字段声明和额外的工作线程函数。


如果不熟悉委托,可以简单的把理解安全的函。采用了委托用,代如下:


// 行任的委托
delegate void RunTaskDelegate( int seconds );

// 过创建委托解决传递参数问题
private void button1_Click( object sender, System.EventArgs )
{
    RunTaskDelegate runTask 
new RunTaskDelegate( RunTask );

    
// 委托同步用方式
    runTask( Convert.ToInt16( txtSecond.Value );
}

//通过创建委托解决传递参数问题,通委托的用消除用界面线程的阻塞问题
private void button1_Click( object sender, System.EventArgs )
{
    RunTaskDelegate runTask 
new RunTaskDelegate( RunTask );

    
// 委托用方式
    runTask.BeginInvoke( Convert.ToInt16( _txtSecond.Value ), nullnull );
}


多线程安全

到这里为止,我们已经解决了长任务的难题传递参数的困扰。但是我们真的解决了全部问题吗?回答是否定的。

我们知道 Windows 编程中有一个必须遵守的原则,那就是在一个窗体创建线程之外的任何线程中都不允许操作窗体。

我们上面的程序就是存在这样的问题:工作线程是在 ShowProgress 方法中修改了用户界面的进度条的属性。那为什么程序运行没有出现问题,运行正常呢?

没有发生问题是因为是现在的Windows XP操作系统对这类问题有非常健壮的解决方法,让我们避免了问题的发生。但是我们现在的程序不能保证在其他的操作系统能够运行正常!
真正的解决方法是我们能够认识到问题所在,并在程序中加以避免。




如何避免多线程的窗体资源访问的安全问题呢?其实非常简单,有两种方法:
1.一种方法就是不管线程是否是用户界面线程,对用户界面资源的访问统一由委托完成;
2.另 一种方法是在每个 Windows Forms 用户界面类中都有一个 InvokeRequired 属性,它用来标识当前线程是否能够直接访问窗体资源。我们只需要检查这个属性的值,只有当允许直接访问窗体资源时才直接访问相应的资源,否则,就需要通过 委托进行访问了。

采用第一种安全的方法的代码片断如下:

// 显示进度条的委托声明
delegate void ShowProgressDelegate( int totalStep, int currentStep );
// 显示进度条
void ShowProgress( int totalStep, int currentStep 
{
    _Progress.Maximum totalStep;
    _Progress.Value currentStep;
}
// 执行任务的委托声明
delegate void RunTaskDelegate( int seconds );       
// 执行任务
void RunTask( int seconds )
{
    ShowProgressDelegate showProgress 
new ShowProgressDelegate( ShowProgress );
    
// 每 秒 显示进度一次
    forint 0; seconds 4; i++ )
    {
        Thread.Sleep( 250 );
        
// 显示进度条
        this.Invoke( showProgress, new object[] seconds 4, );
    }
}


采用第二种安全的方法的代码片断如下:

// 显示进度条的委托声明
delegate void ShowProgressDelegate( int totalStep, int currentStep );
// 显示进度条
void ShowProgress( int totalStep, int currentStep 
{
    
ifProgress.InvokeRequired )
    {
        ShowProgressDelegate showProgress 
new ShowProgressDelegate( ShowProgress );
        
// 为了避免工作线程被阻塞,采用异步调用委托
        this.BeginInvoke( showProgress, new object[] totalStep, currentStep );
    }
    
else
    {
        _Progress.Maximum totalStep;
        _Progress.Value currentStep;
    }
}
// 执行任务的委托声明
delegate void RunTaskDelegate( int seconds );
// 执行任务
void RunTask( int seconds )
{
    
// 每 秒 显示进度一次
    forint 0; seconds 4; i++ )
    {
        Thread.Sleep( 250 );
        
// 显示进度条
        ShowProgress( seconds 4, );
    }
}

至此,我们的 线程介绍就结束了,在学习过程中我们用了几个示例说明了如何执行长任务、如何通过多线程异步处理任务进度的显示并解决了多线程的安全性等问题。希望能够给 大家对理解多线程编程、委托的使用、异步调用等方面提供一些帮助,也希望实验室成员间进行进一步的沟通和交流。

0

阅读 收藏 喜欢 打印举报/Report
  

新浪BLOG意见反馈留言板 欢迎批评指正

新浪简介 | About Sina | 广告服务 | 联系我们 | 招聘信息 | 网站律师 | SINA English | 产品答疑

新浪公司 版权所有