.NET多线程编程入门
在.NET多线程编程这个系列我们讲一起来探讨多线程编程的各个方面。首先我将在本篇文章的开始向大家介绍多线程的有关概念以及多线程编程的基础知识;在接下来的文章中,我将逐一讲述。NET平台上多线程编程的知识,诸如System.Threading命名空间的重要类以及方法,并就一些例子程序来作说明。
引言
早期的计算硬件十分复杂,但是操作系统执行的功能确十分的简单。那个时候的操作系统在任一时间点只能执行一个任务,也就是同一时间只能执行一个程序。多个任务的执行必须得轮流执行,在系统里面进行排队等候。由于计算机的发展,要求系统功能越来越强大,这个时候出现了分时操作的概念:每个运行的程序占有一定的处理机时间,当这个占有时间结束后,在等待队列等待处理器资源的下一个程序就开始投入运行。注意这里的程序在占有一定的处理器时间后并没有运行完毕,可能需要再一次或多次分配处理器时间。那么从这里可以看出,这样的执行方式显然是多个程序的并行执行,但是在宏观上,我们感觉到多个任务是同时执行的,因此多任务的概念就诞生了。每个运行的程序都有自己的内存空间,自己的堆栈和环境变量设置。每一个程序对应一个进程,代表着执行一个大的任务。一个进程可以启动另外一个进程,这个被启动的进程称为子进程。父进程和子进程的执行只有逻辑上的先后关系,并没有其他的关系,也就是说他们的执行是独立的。但是,可能一个大的程序(代表着一个大的任务),可以分割成很多的小任务,为了功能上的需要也有可能是为了加快运行的速度,可能需要同一时间执行多个任务(每个任务分配一个多线程来执行相应的任务)。举个例子来说,你正在通过你的 web浏览器查看一些精彩的文章,你需要把好的文章给下载下来,可能有些非常精彩的文章你需要收藏起来,你就用你的打印机打印这些在线的文章。在这里,浏览器一边下载HTML格式的文章,一边还要打印文章。这就是一个程序同时执行多个任务,每个任务分配一个线程来完成。因此我们可以看出一个程序同时执行多个任务的能力是通过多线程来实现的。
多线程VS多任务
正如上面所说的,多任务是相对与操作系统而言,指的是同一时间执行多个程序的能力,虽然这么说,但是实际上在只有一个CPU的条件下不可能同时执行两个以上的程序。CPU在程序之间做高速的切换,使得所有的程序在很短的时间之内可以得到更小的CPU时间,这样从用户的角度来看就好象是同时在执行多个程序。多线程相对于操作系统而言, 指的是可以同时执行同一个程序的不同部分的能力,每个执行的部分被成为线程。所以在编写应用程序时,我们必须得很好的设计以避免不同的线程执行时的相互干扰。这样有助于我们设计健壮的程序,使得我们可以在随时需要的时候添加线程。
线程的概念
线程可以被描述为一个微进程,它拥有起点,执行的顺序系列和一个终点。它负责维护自己的堆栈,这些堆栈用于异常处理,优先级调度和其他一些系统重新恢复线程执行时需要的信息。从这个概念看来,好像线程与进程没有任何的区别,实际上线程与进程是肯定有区别的:
一个完整的进程拥有自己独立的内存空间和数据,但是同一个进程内的线程是共享内存空间和数据的。一个进程对应着一段程序,它是由一些在同一个程序里面独立的同时的运行的线程组成的。线程有时也被称为并行运行在程序里的轻量级进程,线程被称为是轻量级进程是因为它的运行依赖与进程提供的上下文环境,并且使用的是进程的资源。
在一个进程里,线程的调度有抢占式或者非抢占的模式。
在抢占模式下,操作系统负责分配CPU时间给各个进程,一旦当前的进程使用完分配给自己的CPU时间,操作系统将决定下一个占用CPU时间的是哪一个线程。因此操作系统将定期的中断当前正在执行的线程,将CPU分配给在等待队列的下一个线程。所以任何一个线程都不能独占CPU。每个线程占用CPU的时间取决于进程和操作系统。进程分配给每个线程的时间很短,以至于我们感觉所有的线程是同时执行的。实际上,系统运行每个进程的时间有2毫秒,然后调度其他的线程。它同时他维持着所有的线程和循环,分配很少量的CPU时间给线程。 线程的的切换和调度是如此之快,以至于感觉是所有的线程是同步执行的。
调度是什么意思?调度意味着处理器存储着将要执行完CPU时间的进程的状态和将来某个时间装载这个进程的状态而恢复其运行。然而这种方式也有不足之处,一个线程可以在任何给定的时间中断另外一个线程的执行。假设一个线程正在向一个文件做写操作,而另外一个线程中断其运行,也向同一个文件做写操作。 Windows 95/NT, UNIX使用的就是这种线程调度方式。
在非抢占的调度模式下,每个线程可以需要CPU多少时间就占用CPU多少时间。在这种调度方式下,可能一个执行时间很长的线程使得其他所有需要CPU的线程”饿死”。在处理机空闲,即该进程没有使用CPU时,系统可以允许其他的进程暂时使用CPU。占用CPU的线程拥有对CPU的控制权,只有它自己主动释放CPU时,其他的线程才可以使用CPU。一些I/O和Windows 3。x就是使用这种调度策略。
在有些操作系统里面,这两种调度策略都会用到。非抢占的调度策略在线程运行优先级一般时用到,而对于高优先级的线程调度则多采用抢占式的调度策略。如果你不确定系统采用的是那种调度策略,假设抢占的调度策略不可用是比较安全的。在设计应用程序的时候,我们认为那些占用CPU时间比较多的线程在一定的间隔是会释放CPU的控制权的,这时候系统会查看那些在等待队列里面的与当前运行的线程同一优先级或者更高的优先级的线程,而让这些线程得以使用CPU。如果系统找到一个这样的线程,就立即暂停当前执行的线程和激活满足条件的线程。如果没有找到同一优先级或更高级的线程,当前线程还继续占有CPU。当正在执行的线程想释放CPU的控制权给一个低优先级的线程,当前线程就转入睡眠状态而让低优先级的线程占有CPU。
在多处理器系统,操作系统会将这些独立的线程分配给不同的处理器执行,这样将会大大的加快程序的运行。线程执行的效率也会得到很大的提高,因为将线程的分时共享单处理器变成了分布式的多处理器执行。这种多处理器在三维建模和图形处理是非常有用的。
需要多线程吗
我们发出了一个打印的命令,要求打印机进行打印任务,假设这时候计算机停止了响应而打印机还在工作,那岂不是我们的停止手上的事情就等着这慢速的打印机打印?所幸的是,这种情况不会发生,我们在打印机工作的时候还可以同时听音乐或者画图。因为我们使用了独立的多线程来执行这些任务。你可能会对多个用户同时访问数据库或者web服务器感到吃惊,他们是怎么工作的?这是因为为每个连接到数据库或者web服务器的用户建立了独立的线程来维护用户的状态。如果一个程序的运行有一定的顺序,这时候采用这种方式可能会出现问题,甚至导致整个程序崩溃。如果程序可以分成独立的不同的任务,使用多线程,即使某一部分任务失败了,对其他的也没有影响,不会导致整个程序崩溃。
毫无疑问的是,编写多线程程序使得你有了一个利器可以驾奴非多线程的程序,但是多线程也可能成为一个负担或者需要不小的代价。如果使用的不当,会带来更多的坏处。如果一个程序有很多的线程,那么其他程序的线程必然只能占用更少的CPU时间;而且大量的CPU时间是用于线程调度的;操作系统也需要足够的内存空间来维护每个线程的上下文信息;因此,大量的线程会降低系统的运行效率。因此,如果使用多线程的话,程序的多线程必须设计的很好,否则带来的好处将远小于坏处。因此使用多线程我们必须小心的处理这些线程的创建,调度和释放工作。
多线程程序设计提示
有多种方法可以设计多线程的应用程序。正如后面的文章所示,我将给出详细的编程示例,通过这些例子,你将可以更好的理解多线程。线程可以有不同的优先级,举例子来说,在我们的应用程序里面,绘制图形或者做大量运算的同时要接受用户的输入,显然用户的输入需要得到第一时间的响应,而图形绘制或者运算则需要大量的时间,暂停一下问题不大,因此用户输入线程将需要高的悠闲级,而图形绘制或者运算低优先级即可。这些线程之间相互独立,相互不影响。
在上面的例子中,图形绘制或者大量的运算显然是需要站用很多的CPU时间的,在这段时间,用户没有必要等着他们执行完毕再输入信息,因此我们将程序设计成独立的两个线程,一个负责用户的输入,一个负责处理那些耗时很长的任务。这将使得程序更加灵活,能够快速响应。同时也可以使得用户在运行的任何时候取消任务的可能。在这个绘制图形的例子中,程序应该始终负责接收系统发来的消息。如果由于程序忙于一个任务,有可能会导致屏幕变成空白,这显然需要我们的程序来处理这样的事件。所以我必须得有一个线程负责来处理这些消息,正如刚才所说的应该触发重画屏幕的工作。
我们应该把握一个原则,对于那些对时间要求比较紧迫需要立即得到相应的任务,我们因该给予高的优先级,而其他的线程优先级应该低于她的优先级。侦听客户端请求的线程应该始终是高的优先级,对于一个与用户交互的用户界面的任务来说,它需要得到第一时间的响应,其优先级因该高优先级。
在接下来的这篇文章中,我将向大家介绍.NET中的线程API,怎么样用C#创建线程,启动和停止线程,设置优先级和状态.
在. NET中编写的程序将被自动的分配一个线程.让我们来看看用C#编程语言创建线程并且继续学习线程的知识。我们都知道.NET的运行时环境的主线程由 Main ()方法来启动应用程序,而且.NET的编译语言有自动的垃圾收集功能,这个垃圾收集发生在另外一个线程里面,所有的这些都是后台发生的,让我们无法感觉到发生了什么事情.在这里默认的是只有一个线程来完成所有的程序任务,但是正如我们在第一篇文章讨论过的一样,有可能我们根据需要自己添加更多的线程让程序更好的协调工作。比如说我们的例子中,一个有用户输入的同时需要绘制图形或者完成大量的运算的程序,我们必须得增加一个线程,让用户的输入能够得到及时的响应,因为输入对时间和响应的要求是紧迫的,而另外一个线程负责图形绘制或者大量的运算。
.NET 基础类库的System.Threading命名空间提供了大量的类和接口支持多线程。这个命名空间有很多的类,我们将在这里着重讨论Thread这个类。
System.Threading.Thread类是创建并控制线程,设置其优先级并获取其状态最为常用的类。他有很多的方法,在这里我们将就比较常用和重要的方法做一下介绍:
Thread.Start():启动线程的执行;
Thread.Suspend():挂起线程,或者如果线程已挂起,则不起作用;
Thread.Resume():继续已挂起的线程;
Thread.Interrupt():中止处于 Wait或者Sleep或者Join 线程状态的线程;
Thread.Join():阻塞调用线程,直到某个线程终止时为止
Thread.Sleep():将当前线程阻塞指定的毫秒数;
Thread.Abort():以开始终止此线程的过程。如果线程已经在终止,则不能通过Thread.Start()来启动线程。
通过调用Thread.Sleep,Thread.Suspend或者Thread.Join可以暂停/阻塞线程。调用Sleep()和Suspend() 方法意味着线程将不再得到CPU时间。这两种暂停线程的方法是有区别的,Sleep()使得线程立即停止执行,但是在调用Suspend()方法之前,公共语言运行时必须到达一个安全点。一个线程不能对另外一个线程调用Sleep()方法,但是可以调用Suspend()方法使得另外一个线程暂停执行。对已经挂起的线程调用Thread.Resume()方法会使其继续执行。不管使用多少次Suspend()方法来阻塞一个线程,只需一次调用Resume ()方法就可以使得线程继续执行。已经终止的和还没有开始执行的线程都不能使用挂起。Thread.Sleep(int x)使线程阻塞x毫秒。只有当该线程是被其他的线程通过调用Thread.Interrupt()或者Thread.Abort()方法,才能被唤醒。如果对处于阻塞状态的线程调用Thread.Interrupt()方法将使线程状态改变,但是会抛出 ThreadInterupptedException异常,你可以捕获这个异常并且做出处理,也可以忽略这个异常而让运行时终止线程。在一定的等待时间之内,Thread.Interrupt()和Thread.Abort()都可以立即唤醒一个线程。
下面我们将说明如何从一个线程中止另外一个线程。在这种情况下,我们可以通过使用Thread.Abort()方法来永久销毁一个线程,而且将抛出 ThreadAbortException异常。使终结的线程可以捕获到异常但是很难控制恢复,仅有的办法是调用Thread.ResetAbort()来取消刚才的调用,而且只有当这个异常是由于被调用线程引起的异常。因此,A线程可以正确的使用Thread.Abort()方法作用于B线程,但是B线程却不能调用Thread.ResetAbort()来取消Thread.Abort()操作。Thread.Abort()方法使得系统悄悄的销毁了线程而且不通知用户。一旦实施Thread.Abort()操作,该线程不能被重新启动。调用了这个方法并不是意味着线程立即销毁,因此为了确定线程是否被销毁,我们可以调用Thread.Join()来确定其销毁,Thread.Join()是一个阻塞调用,直到线程的确是终止了才返回。但是有可能一个线程调用Thread.Interrupt()方法来中止另外一个线程,而这个线程正在等待Thread.Join()调用的返回。
尽可能的不要用Suspend()方法来挂起阻塞线程,因为这样很容易造成死锁。假设你挂起了一个线程,而这个线程的资源是其他线程所需要的,会发生什么后果。因此,我们尽可能的给重要性不同的线程以不同的优先级,用Thread.Priority()方法来代替使用Thread.Suspend()方法。
Thread类有很多的属性,这些重要的属性是我们多线程编程必须得掌握的。
Thread.IsAlive属性:获取一个值,该值指示当前线程的执行状态。如果此线程已启动并且尚未正常终止或中止,则为 true;否则为 false。
Thread.Name 属性:获取或设置线程的名称。
Thread.Priority 属性:获取或设置一个值,该值指示线程的调度优先级。
Thread.ThreadState 属性:获取一个值,该值包含当前线程的状态。
在下面的例子中,我们将看看怎么设置这些属性,在随后的例子中我们将详细的讨论这些属性。
创建一个线程,首先得实例化一个Thread类,在类得构造函数中调用ThreadStart委派。这个委派包含了线程从哪里开始执行。当线程启动后,Start()方法启动一个新的线程。下面是例子程序。
using System;
using System.Threading ;
namespace LearnThreads
{
class Thread_App
{
public static void First_Thread()
{
Console.WriteLine("First thread created");
Thread current_thread = Thread.CurrentThread;
string thread_details = "Thread Name: " + current_thread.Name +
"\r\nThread State: " + current_thread.ThreadState.ToString()+
"\r\n Thread Priority level:"+current_thread.Priority.ToString();
Console.WriteLine("The details of the thread are :"+ thread_details);
Console.WriteLine ("first thread terminated");
}
public static void Main()
{
ThreadStart thr_start_func = new ThreadStart (First_Thread);
Console.WriteLine ("Creating the first thread ");
Thread fThread = new Thread (thr_start_func);
fThread.Name = "first_thread";
fThread.Start (); //starting the thread
}
}
}
在这个例子中,创建了一个fThread的线程对象,这个线程负责执行First_Thread()方法里面的任务。当Thread的Start() 方法被调用时包含First_Thread()的地址ThreadStart的代理将被执行。
Thread状态
System.Threading.Thread.ThreadState 属性定义了执行时线程的状态。线程从创建到线程终止,它一定处于其中某一个状态。当线程被创建时,它处在Unstarted状态,Thread类的 Start() 方法将使线程状态变为Running状态,线程将一直处于这样的状态,除非我们调用了相应的方法使其挂起、阻塞、销毁或者自然终止。如果线程被挂起,它将处于Suspended状态,除非我们调用resume()方法使其重新执行,这时候线程将重新变为Running状态。一旦线程被销毁或者终止,线程处于Stopped状态。处于这个状态的线程将不复存在,正如线程开始启动,线程将不可能回到Unstarted状态。线程还有一个Background状态,它表明线程运行在前台还是后台。在一个确定的时间,线程可能处于多个状态。据例子来说,一个线程被调用了Sleep而处于阻塞,而接着另外一个线程调用Abort方法于这个阻塞的线程,这时候线程将同时处于WaitSleepJoin和AbortRequested状态。一旦线程响应转为Sle阻塞或者中止,当销毁时会抛出ThreadAbortException异常。
线程优先级
System.Threading.Thread.Priority 枚举了线程的优先级别,从而决定了线程能够得到多少CPU时间。高优先级的线程通常会比一般优先级的线程得到更多的CPU时间,如果不止一个高优先级的线程,操作系统将在这些线程之间循环分配CPU时间。低优先级的线程得到的CPU时间相对较少,当这里没有高优先级的线程,操作系统将挑选下一个低优先级的线程执行。一旦低优先级的线程在执行时遇到了高优先级的线程,它将让出CPU给高优先级的线程。新创建的线程优先级为一般优先级,我们可以设置线程的优先级别的值,如下面所示:
Highest
AboveNormal
Normal
BelowNormal
Lowest
结论:在这一部分,我们讨论了线程的创建何线程的优先级。System.Threading命名空间还包含了线程锁定、线程同步何通讯、多线程管理类以及死锁解决等等高级特性,在后面的部分我们将继续讨论这些内容。
随着对多线程学习的深入,你可能觉得需要了解一些有关线程共享资源的问题. .NET framework提供了很多的类和数据类型来控制对共享资源的访问。
考虑一种我们经常遇到的情况:有一些全局变量和共享的类变量,我们需要从不同的线程来更新它们,可以通过使用System.Threading.Interlocked类完成这样的任务,它提供了原子的,非模块化的整数更新操作。
还有你可以使用System.Threading.Monitor类锁定对象的方法的一段代码,使其暂时不能被别的线程访问。
System.Threading.WaitHandle类的实例可以用来封装等待对共享资源的独占访问权的操作系统特定的对象。尤其对于非受管代码的互操作问题。
System.Threading.Mutex用于对多个复杂的线程同步的问题,它也允许单线程的访问。
像ManualResetEvent和AutoResetEvent这样的同步事件类支持一个类通知其他事件的线程。
不讨论线程的同步问题,等于对多线程编程知之甚少,但是我们要十分谨慎的使用多线程的同步。在使用线程同步时,我们事先就要要能够正确的确定是那个对象和方法有可能造成死锁(死锁就是所有的线程都停止了相应,都在等者对方释放资源)。还有赃数据的问题(指的是同一时间多个线程对数据作了操作而造成的不一致),这个不容易理解,这么说吧,有X和Y两个线程,线程X从文件读取数据并且写数据到数据结构,线程Y从这个数据结构读数据并将数据送到其他的计算机。假设在Y读数据的同时,X写入数据,那么显然Y读取的数据与实际存储的数据是不一致的。这种情况显然是我们应该避免发生的。少量的线程将使得刚才的问题发生的几率要少的多,对共享资源的访问也更好的同步。
.NET Framework的CLR提供了三种方法来完成对共享资源 ,诸如全局变量域,特定的代码段,静态的和实例化的方法和域。
(1) 代码域同步:使用Monitor类可以同步静态/实例化的方法的全部代码或者部分代码段。不支持静态域的同步。在实例化的方法中,this指针用于同步;而在静态的方法中,类用于同步,这在后面会讲到。
(2)手工同步:使用不同的同步类(诸如WaitHandle, Mutex, ReaderWriterLock, ManualResetEvent, AutoResetEvent 和Interlocked等)创建自己的同步机制。这种同步方式要求你自己手动的为不同的域和方法同步,这种同步方式也可以用于进程间的同步和对共享资源的等待而造成的死锁解除。
(3) 上下文同步:使用SynchronizationAttribute为ContextBoundObject对象创建简单的,自动的同步。这种同步方式仅用于实例化的方法和域的同步。所有在同一个上下文域的对象共享同一个锁。
Monitor Class
在给定的时间和指定的代码段只能被一个线程访问,Monitor 类非常适合于这种情况的线程同步。这个类中的方法都是静态的,所以不需要实例化这个类。下面一些静态的方法提供了一种机制用来同步对象的访问从而避免死锁和维护数据的一致性。
Monitor.Enter 方法:在指定对象上获取排他锁。
Monitor.TryEnter 方法:试图获取指定对象的排他锁。
Monitor.Exit 方法:释放指定对象上的排他锁。
Monitor.Wait 方法:释放对象上的锁并阻塞当前线程,直到它重新获取该锁。
Monitor.Pulse 方法:通知等待队列中的线程锁定对象状态的更改。
Monitor.PulseAll 方法:通知所有的等待线程对象状态的更改。
通过对指定对象的加锁和解锁可以同步代码段的访问。Monitor.Enter, Monitor.TryEnter 和 Monitor.Exit用来对指定对象的加锁和解锁。一旦获取(调用了Monitor.Enter)指定对象(代码段)的锁,其他的线程都不能获取该锁。举个例子来说吧,线程X获得了一个对象锁,这个对象锁可以释放的(调用Monitor.Exit(object) or Monitor.Wait)。当这个对象锁被释放后,Monitor.Pulse方法和 Monitor.PulseAll方法通知就绪队列的下一个线程进行和其他所有就绪队列的线程将有机会获取排他锁。线程X释放了锁而线程Y获得了锁,同时调用Monitor.Wait的线程X进入等待队列。当从当前锁定对象的线程(线程Y)受到了Pulse或PulseAll,等待队列的线程就进入就绪队列。线程X重新得到对象锁时,Monitor.Wait才返回。如果拥有锁的线程(线程Y)不调用Pulse或PulseAll,方法可能被不确定的锁定。Pulse, PulseAll and Wait必须是被同步的代码段鄂被调用。对每一个同步的对象,你需要有当前拥有锁的线程的指针,就绪队列和等待队列(包含需要被通知锁定对象的状态变化的线程)的指针。
你也许会问,当两个线程同时调用Monitor.Enter会发生什么事情?无论这两个线程地调用 Monitor.Enter是多么地接近,实际上肯定有一个在前,一个在后,因此永远只会有一个获得对象锁。既然Monitor.Enter是原子操作,那么CPU是不可能偏好一个线程而不喜欢另外一个线程的。为了获取更好的性能,你应该延迟后一个线程的获取锁调用和立即释放前一个线程的对象锁。对于 private和internal的对象,加锁是可行的,但是对于external对象有可能导致死锁,因为不相关的代码可能因为不同的目的而对同一个对象加锁。
如果你要对一段代码加锁,最好的是在try语句里面加入设置锁的语句,而将Monitor.Exit放在finally语句里面。对于整个代码段的加锁,你可以使用MethodImplAttribute(在System.Runtime.CompilerServices命名空间)类在其构造器中设置同步值。这是一种可以替代的方法,当加锁的方法返回时,锁也就被释放了。如果需要要很快释放锁,你可以使用Monitor类和 C# lock的声明代替上述的方法。
让我们来看一段使用Monitor类的代码:
public void some_method()
{
int a=100;
int b=0;
Monitor.Enter(this);
//say we do something here.
int c=a/b;
Monitor.Exit(this);
}
上面的代码运行会产生问题。当代码运行到int c=a/b; 的时候,会抛出一个异常,Monitor.Exit将不会返回。因此这段程序将挂起,其他的线程也将得不到锁。有两种方法可以解决上面的问题。第一个方法是:将代码放入try…finally内,在finally调用Monitor.Exit,这样的话最后一定会释放锁。第二种方法是:利用C#的lock ()方法。调用这个方法和调用Monitoy.Enter的作用效果是一样的。但是这种方法一旦代码执行超出范围,释放锁将不会自动的发生。见下面的代码:
public void some_method()
{
int a=100;
int b=0;
lock(this);
//say we do something here.
int c=a/b;
}
C# lock申明提供了与Monitoy.Enter和Monitoy.Exit同样的功能,这种方法用在你的代码段不能被其他独立的线程中断的情况。
WaitHandle Class
WaitHandle 类作为基类来使用的,它允许多个等待操作。这个类封装了win32的同步处理方法。WaitHandle对象通知其他的线程它需要对资源排他性的访问,其他的线程必须等待,直到WaitHandle不再使用资源和等待句柄没有被使用。下面是从它继承来的几个类:
Mutex 类:同步基元也可用于进程间同步。
AutoResetEvent:通知一个或多个正在等待的线程已发生事件。无法继承此类。
ManualResetEvent:当通知一个或多个正在等待的线程事件已发生时出现。无法继承此类。
这些类定义了一些信号机制使得对资源排他性访问的占有和释放。他们有两种状态:signaled 和 nonsignaled。Signaled状态的等待句柄不属于任何线程,除非是nonsignaled状态。拥有等待句柄的线程不再使用等待句柄时用 set方法,其他的线程可以调用Reset方法来改变状态或者任意一个WaitHandle方法要求拥有等待句柄,这些方法见下面:
WaitAll:等待指定数组中的所有元素收到信号。
WaitAny:等待指定数组中的任一元素收到信号。
WaitOne:当在派生类中重写时,阻塞当前线程,直到当前的 WaitHandle 收到信号。
这些wait方法阻塞线程直到一个或者更多的同步对象收到信号。
WaitHandle对象封装等待对共享资源的独占访问权的操作系统特定的对象无论是收管代码还是非受管代码都可以使用。但是它没有Monitor使用轻便,Monitor是完全的受管代码而且对操作系统资源的使用非常有效率。
Mutex Class
Mutex 是另外一种完成线程间和跨进程同步的方法,它同时也提供进程间的同步。它允许一个线程独占共享资源的同时阻止其他线程和进程的访问。Mutex的名字就很好的说明了它的所有者对资源的排他性的占有。一旦一个线程拥有了Mutex,想得到Mutex的其他线程都将挂起直到占有线程释放它。 Mutex.ReleaseMutex方法用于释放Mutex,一个线程可以多次调用wait方法来请求同一个Mutex,但是在释放Mutex的时候必须调用同样次数的Mutex.ReleaseMutex。如果没有线程占有Mutex,那么Mutex的状态就变为signaled,否则为 nosignaled。一旦Mutex的状态变为signaled,等待队列的下一个线程将会得到Mutex。Mutex类对应与win32的 CreateMutex,创建Mutex对象的方法非常简单,常用的有下面几种方法:
一个线程可以通过调用 WaitHandle.WaitOne 或 WaitHandle.WaitAny 或 WaitHandle.WaitAll得到Mutex的拥有权。如果Mutex不属于任何线程,上述调用将使得线程拥有Mutex,而且WaitOne会立即返回。但是如果有其他的线程拥有Mutex,WaitOne将陷入无限期的等待直到获取Mutex。你可以在WaitOne方法中指定参数即等待的时间而避免无限期的等待Mutex。调用Close作用于Mutex将释放拥有。一旦Mutex被创建,你可以通过GetHandle方法获得Mutex的句柄而给WaitHandle.WaitAny 或 WaitHandle.WaitAll 方法使用。
下面是一个示例:
public void some_method()
{
int a=100;
int b=20;
Mutex firstMutex = new Mutex(false);
FirstMutex.WaitOne();
//some kind of processing can be done here.
Int x=a/b;
FirstMutex.Close();
}
在上面的例子中,线程创建了Mutex,但是开始并没有申明拥有它,通过调用WaitOne方法拥有Mutex。
Synchronization Events
同步时间是一些等待句柄用来通知其他的线程发生了什么事情和资源是可用的。他们有两个状态:signaled and nonsignaled。AutoResetEvent 和 ManualResetEvent就是这种同步事件。
AutoResetEvent Class
这个类可以通知一个或多个线程发生事件。当一个等待线程得到释放时,它将状态转换为signaled。用set方法使它的实例状态变为signaled。但是一旦等待的线程被通知时间变为signaled,它的转台将自动的变为nonsignaled。如果没有线程侦听事件,转台将保持为signaled。此类不能被继承。
ManualResetEvent Class
这个类也用来通知一个或多个线程事件发生了。它的状态可以手动的被设置和重置。手动重置时间将保持signaled状态直到ManualResetEvent.Reset 设置其状态为nonsignaled,或保持状态为nonsignaled直到ManualResetEvent.Set设置其状态为signaled。这个类不能被继承。
Interlocked Class
它提供了在线程之间共享的变量访问的同步,它的操作时原子操作,且被线程共享.你可以通过Interlocked.Increment 或 Interlocked.Decrement来增加或减少共享变量.它的有点在于是原子操作,也就是说这些方法可以代一个整型的参数增量并且返回新的值, 所有的操作就是一步.你也可以使用它来指定变量的值或者检查两个变量是否相等,如果相等,将用指定的值代替其中一个变量的值.
ReaderWriterLock class
它定义了一种锁,提供唯一写/多读的机制,使得读写的同步.任意数目的线程都可以读数据,数据锁在有线程更新数据时将是需要的.读的线程可以获取锁,当且仅当这里没有写的线程.当没有读线程和其他的写线程时,写线程可以得到锁.因此,一旦writer-lock被请求,所有的读线程将不能读取数据直到写线程访问完毕.它支持暂停而避免死锁.它也支持嵌套的读/写锁.支持嵌套的读锁的方法是 ReaderWriterLock.AcquireReaderLock,如果一个线程有写锁则该线程将暂停;
支持嵌套的写锁的方法是 ReaderWriterLock.AcquireWriterLock,如果一个线程有读锁则该线程暂停.如果有读锁将容易倒是死锁.安全的办法是使用 ReaderWriterLock.UpgradeToWriterLock方法,这将使读者升级到写者.你可以用 ReaderWriterLock.DowngradeFromWriterLock方法使写者降级为读者.调用 ReaderWriterLock.ReleaseLock将释放锁, ReaderWriterLock.RestoreLock将重新装载锁的状态到调用ReaderWriterLock.ReleaseLock以前.
结论:
这部分讲述了.NET平台上的线程同步的问题.造接下来的系列文章中我将给出一些例子来更进一步的说明这些使用的方法和技巧.虽然线程同步的使用会给我们的程序带来很大的价值,但是我们最好能够小心使用这些方法.否则带来的不是受益,而将倒是性能下降甚至程序崩溃.只有大量的联系和体会才能使你驾驭这些技巧.尽量少使用那些在同步代码块完成不了或者不确定的阻塞的东西,尤其是I/O操作;尽可能的使用局部变量来代替全局变量;同步用在那些部分代码被多个线程和进程访问和状态被不同的进程共享的地方;安排你的代码使得每一个数据在一个线程里得到精确的控制;不是共享在线程之间的代码是安全的;在下一篇文章中我们将学习线程池有关的知识.
如果你仔细阅读了我前面的三篇文章,我相信你对用.NET Framework提供的System.Threading.Thread类和一些线程同步的类基本的线程知识和多线程编程知识很了解。我们将在这里进一步讨论一些.NET类,以及他们在多线程编程中扮演的角色和怎么编程。它们是:
System.Threading.ThreadPool 类
System.Threading.Timer 类
如果线程的数目并不是很多,而且你想控制每个线程的细节诸如线程的优先级等,使用Thread是比较合适的;但是如果有大量的线程,考虑使用线程池应该更好一些,它提供了高效的线程管理机制来处理多任务。 对于定期的执行任务Timer类是合适的;使用代表是异步方法调用的首选。
System.Threading.ThreadPool Class
当你创建应用程序时,你应该认识到大部分时间你的线程在空闲的等待某些事件的发生(诸如按下一个键或侦听套节子的请求)。毫无疑问的,你也会认为这是绝对的浪费资源。
如果这里有很多的任务需要完成,每个任务需要一个线程,你应该考虑使用线程池来更有效的管理你的资源并且从中受益。线程池是执行的多个线程集合,它允许你添加以线程自动创建和开始的任务到队列里面去。使用线程池使得你的系统可以优化线程在CPU使用时的时间碎片。但是要记住在任何特定的时间点,每一个进程和每个线程池只有一个一个正在运行的线程。这个类使得你的线程组成的池可以被系统管理,而使你的主要精力集中在工作流的逻辑而不是线程的管理。
当第一次实例化ThreadPool类时线程池将被创建。它有一个默认的上限,即每处理器最多可以有25个,但是这个上限是可以改变的。这样使得处理器不会闲置下来。如果其中一个线程等待某个事件的发生,线程池将初始化另外一个线程并投入处理器工作,线程池就是这样不停的创建工作的线程和分配任务给那些没有工作的在队列里的线程。唯一的限制是工作线程的数目不能超过最大允许的数目。每个线程将运行在默认的优先级和使用默认的属于多线程空间的堆栈大小空间。一旦一项工作任务被加入队列,你是不能取消的。
请求线程池处理一个任务或者工作项可以调用QueueUserWorkItem方法。这个方法带一个WaitCallback代表类型的参数,这个参数包装了你药完成的任务。运行时自动为每一个的任务创建线程并且在任务释放时释放线程。
下面的代码说明了如何创建线程池和怎样添加任务:
public void afunction(object o)
{
// do what ever the function is supposed to do.
}
//thread entry code
{
// create an instance of WaitCallback
WaitCallback myCallback = new WaitCallback (afunction);
//add this to the thread pool / queue a task
ThreadPool.QueueUserWorkItem (myCallback);
}
你也可以通过调用ThreadPool.RegisterWaitForSingleObject方法来传递一个 System.Threading.WaitHandle,当被通知或者时间超过了调用被 System.Threading.WaitOrTimerCallback包装的方法。
线程池和基于事件的编程模式使得线程池对注册的WaitHandles的监控和对合适的WaitOrTimerCallback代表方法的调用十分简单(当WaitHandle被释放时)。这些做法其实很简单。这里有一个线程不断的观测在线程池队列等待操作的状态。一旦等待操作完成,一个线程将被执行与其对应的任务。因此,这个方法随着出发触发事件的发生而增加一个线程。
让我们看看怎么随事件添加一个线程到线程池,其实很简单。我们只需要创建一个 ManualResetEvent类的事件和一个WaitOrTimerCallback的代表,然后我们需要一个携带代表状态的对象,同时我们也要决定休息间隔和执行方式。我们将上面的都添加到线程池,并且激发一个事件:
public void afunction(object o)
{
// do what ever the function is supposed to do.
}
//object that will carry the status info?O:P>
public class anObject
{
}
//thread entry code
{
//create an event object?
ManualResetEvent aevent = new ManualResetEvent (false);
// create an instance of WaitOrTimerCallback
WaitOrTimerCallback thread_method = new WaitOrTimerCallback (afunction);
// create an instance of anObject
anObject myobj = new anObject();
// decide how thread will perform
int timeout_interval = 100; // timeout in milli-seconds.
bool onetime_exec = true;
//add all this to the thread pool.
ThreadPool. RegisterWaitForSingleObject (aevent, thread_method, myobj, timeout_interval, onetime_exec);
// raise the event
aevent.Set();
}
在QueueUserWorkItem 和RegisterWaitForSingleObject方法中,线程池创建了一个后台的线程来回调。当线程池开始执行一个任务,两个方法都将调用者的堆栈合并到线程池的线程堆栈中。如果需要安全检查将耗费更多的时间和增加系统的负担,因此可以通过使用它们对应的不安全的方法来避免安全检查。就是 ThreadPool.UnsafeRegisterWaitForSingleObject 和ThreadPool.UnsafeQueueUserWorkItem。
你也可以对与等待操作无关的任务排队。 Timer-queue timers and registered wait operations也使用线程池。它们的返回方法也被放入线程池排队。
线程池是非常有用的,被广泛的用于。NET平台上的套节子编程,等待操作注册,进程计时器和异步的I/O。对于小而短的任务,线程池提供的机制也是十分便利处于多线程的。线程池对于完成许多独立的任务而且不需要逐个的设置线程属性是十分便利的。但是,你也应该很清楚,有很多的情况是可以用其他的方法来替代线程池的。比如说你的计划任务或给每个线程特定的属性,或者你需要将线程放入单个线程的空间(而线程池是将所有的线程放入一个多线程空间),抑或是一个特定的任务是很冗长的,这些情况你最好考虑清楚,安全的办法比用线程池应该是你的选择。
System.Threading.Timer Class
Timer类对于周期性的在分离的线程执行任务是非常有效的,它不能被继承。
这个类尤其用来开发控制台应用程序,因为System.Windows.Forms.Time是不可用的。比如同来备份文件和检查数据库的一致性。
当创建Timer对象时,你药估计在第一个代理调用之前等待的时间和后来的每次成功调用之间的时间。一个定时调用发生在方法的应得时间过去,并且在后来周期性的调用这个方法。你可以适应Timer的Change方法来改变这些设置的值或者使Timer失效。当定时器Timer不再使用时,你应该调用 Dispose方法来释放其资源。
TimerCallback代表负责指定与Timer对象相关联的方法(就是要周期执行的任务)和状态。它在方法应得的时间过去之后调用一次并且周期性的调用这个方法直到调用了Dispose方法释放了Timer的所有资源。系统自动分配分离的线程。
让我们来看一段代码看看事如何创建Timer对象和使用它的。我们首先要创建一个TimerCallback代理,在后面的方法中要使用到的。如果需要,下一步我们要创建一个状态对象,它拥有与被代理调用的方法相关联的特定信息。为了使这些简单一些,我们传递一个空参数。我们将实例化一个Timer对象,然后再使用Change方法改变Timer的设置,最后调用Dispose方法释放资源。
// class that will be called by the Timer
public class WorkonTimerReq
{
public void aTimerCallMethod()
{
// does some work ?
}
}
//timer creation block
{
//instantiating the class that gets called by the Timer.
WorkonTimerReq anObj = new WorkonTimerReq () ;
// callback delegate
TimerCallback tcallback = new TimerCallback(anObj. aTimerCallMethod) ;
// define the dueTime and period
long dTime = 20 ; // wait before the first tick (in ms)
long pTime = 150 ; // timer during subsequent invocations (in ms)
// instantiate the Timer object
Timer atimer = new Timer(tcallback, null, dTime, pTime) ;
// do some thing with the timer object
...
//change the dueTime and period of the Timer
dTime=100;
pTime=300;
atimer.Change(dTime, pTime) ;
// do some thing
...
atimer.Dispose() ;
...
}
异步编程
这部分内容如果要讲清楚本来就是很大的一部分,在这里,我不打算详细讨论这个东西,我们只是需要直到它是什么,因为多线程编程如果忽律异步的多线程编程显然是不应该的。异步的多线程编程是你的程序可能会用到的另外一种多线程编程方法。
在前面的文章我们花了很大的篇幅来介绍线程的同步和怎么实现线程的同步,但是它有一个固有的致命的缺点,你或许注意到了这一点。那就是每个线程必须作同步调用,也就是等到其他的功能完成,否则就阻塞。当然,某些情况下,对于那些逻辑上相互依赖的任务来说是足够的。异步编程允许更加复杂的灵活性。一个线程可以作异步调用,不需要等待其他的东西。你可以使用这些线程作任何的任务,线程负责获取结果推进运行。这给予了那些需要管理数目巨大的请求而且负担不起请求等待代价的企业级的系统更好的可伸缩性。
.NET平台提供了一致的异步编程机制用于ASP.NET,I/O,Web Services,Networking,Message等。
后记
由于学习的时候很难找到中文这方面的资料,因此我就只好学习英文的资料,由于水平不高,翻译的时候可能难免曲解原文的意思,希望大家能够指出,同时希望这些东西能够给大家在学习这方面知识给予一定的参考和帮助,那怕是一点点,就很欣慰了。