(一)
通过System.Threading命名空间中提供的Thread类来创建和控制线程,ThreadPool类用于管理线程池等;
Thread类有几个至关重要的方法,描述如下:
Start(); 启动线程
Sleep(int); 静态方法,暂停当前线程制定的毫秒数
Abort(); 通常使用该方法来终止一个线程
Join(); 等待直到线程结束
Suspend(); 该方法并不终止未完成的线程,它仅仅挂起线程,以后还可以恢复
Resume(); 恢复被Suspend()方法挂起的线程执行。
(二)如何操纵一个线程
使用Thread类创建线程时,只需要提供线程入口即可。
在C#中,线程入口是通过ThreadStart代理(delegate)来提供的,当调用Thread.Start()方法后,线程就开始执行ThreadStart所指向的函数。
Thread oThread = new Thread(new ThreadStart(oAlpha.Beta));
在创建线程oThread时,我们用指向oAlpha.Beta()方法初始化了ThreadStart代理(delegate)对象,当我们创建线程oThread调用oThread.Start()方法启动时,实际上程序运行的是oAlpha.Beta()方法。
在Main方法中的while循环中,使用静态方法Thread.Sleep()让主线程暂停一段时间,这段时间CPU转向执行线程oThread。然后我们使用Thread.Abort()方法终止了线程oThread,注意后面的oThread.Join(),Thread.Join()方法使主线程等待,直到oThread线程结束。也可以给Thread.Join()方法指定一个int型的参数作为最长的等待时间。
之后我们试图用Thread.Start方法重新启动线程oThread,但是,显然Abort()方法终止后的线程不可恢复,所以程序最后抛出ThreadStateException异常。
Thread.ThreadState 属性
这个属性代表了程序运行时的状态。
ThreadState属性的取值如下:
Aborted 线程已停止
AbortRequested 线程的Thread.Abort()方法已被调用,但是线程还未停止
Background 线程在后台执行,与属性Thread.IsBackground有关
Running 线程正在正常运行
Stopped 线程已经被停止
StopRequested 线程正在被要求停止
Suspended 线程已经被挂起(在此状态下,可以通过Resume()方法重新运行)
SuspendRequest 线程正在要求被挂起,但是还未来得及相应
Unstarted 未调用Thread.Start()开始线程的运行
WaitSleepJoin 线程因为调用了Wait(),Sleep()或Join()等方法处于封锁状态。
注:后台线程与前台线程只有一个区别,那就是后台线程不妨碍程序的终止。一旦一个进程所有的前台线程都终止后,CLR(通用语言运行环境)将调用任意一个存活中的后台进程的Abort()方法来彻底终止进程。
线程的优先级
当线程之间争夺CPU时间时,CPU是按照线程的优先级给予服务的。在C#程序中,用户可以设定5个不同的优先级,由高到低分别是Highest,AboveNormal,Normal,BelowNormal,Lowest,在创建线程时如果不指定优先级,那么系统默认为ThreadPriority.Normal。
(三)生产者和消费者
每个线程都有自己的资源,但是代码区是共享的,即每个线程都可以执行相同的函数。几个线程同时执行一个函数,会导致数据混乱,产生不可预料的结果。必须避免这种情况的发生。
C#提供了一个关键字lock,可以把一段代码定义为互斥段(critical section),互斥段在一个时刻内只允许一个线程进入执行,而其他线程必须等待。在C#中,关键字lock定义入下:
lock(expression) statement_block
expression代表你希望跟踪的对象,通常是对象引用。如果你想保护一个类的实例,一般的,可以使用this;保护一个静态变量(如互斥代码段在一个静态方法内部),一般使用类名就可以了。
statement_block就是互斥代码段,这段代码在一个时刻内只可能被一个线程执行。
下例使用线程和
lock。只要
lock
语句存在,语句块就是临界区并且
balance
永远不会是负数。
using System;
using System.Threading;
class Account
{
}
class Test
{
}
Monitor类锁定对象:
Monitor常用的方法:
Enter() 锁定对象
Exit() 释放对象
Pulse() 通知等待队列中的线程锁定对象状态的改变
Wait() 释放对象上的锁并组织当前线程,直到它重新获取该锁
注1:当拥有对象锁的线程准备释放锁时,它使用Monitor.Pulse()方法通知等待队列中的第一个线程,于是该线程被转移到预备队列中,当对象锁被释放时,在预备队列中的线程可以立即获得对象锁。
当多线程公用一个对象时,应使用Monitor类,其提供了使线程共享资源的方案。Monitor类可以锁定一个对象,一个线程只有得到这把锁才可以对该对象进行操作。保证了一个时刻只有一个线程可以访问这个对象。Monitor必须和一个具体的对象相关联,但是由于它是一个静态的类,所以不能使用它来定义对象,而且它的所有办法都是静态的,不能使用对象来引用。下面的代码说明了使用Monitor锁定一个对象的情形:
。。。
Queue oQueue=new Queue();
....
Monitor.Enter(oQueue);
....//现在oQueue对象只能被当前线程操纵了
Monitor.Exit(oQueue);//释放锁
其他线程只有等待它使用Monitor.Exit()方法释放锁后才可以使用。为了保证线程最终都能释放锁,可以把Monitor.Exit()方法写在try-catch-finally结构的finally代码块里。
对于任何一个被Monitor锁定的对象,内存中都保存着与它相关的一些信息:现在持有锁的线程的引用;预备队列,队列中保存了已经准备好获取锁的线程;等待队列,队列中保存着当前正在等待这个对象状态改变的线程的引用。
当拥有对象锁的线程准备释放锁时,它使用Monitor.Pulse()方法通知等待队列中的第一个线程,于是该线程被转移到预备队列中,当对象锁被释放时,在预备队列中的线程可以立即获得对象锁。
下面是一个展示如何使用lock关键字和Monitor类来实现线程的同步和通讯的例子,也是一个典型的生产者与消费者问题。这个历程中,生产者线程和消费者线程是交替进行的,生产者写入一个数,消费者立即读取并且显示:
namespace Ex2
{
}
在上面的例程中,同步是等待Monitor.Pulse()来完成的。首先生产者生产了一个值,而同一时刻消费者处于等待状态,直到收到生产者的“脉冲(Pulse)”通知它生产已经完成,此后消费者进入消费状态,而生产者开始等待消费者完成操作后将调用Monitor.Pulse()发送的“脉冲”。
程序执行过程:线程通过 lock(this) 即Enter方法获得对象cell的锁(或者等待获得),首先判断数据是否存在,即readFlag的值。
如果为false,即没有数据可以读取,producer线程执行写数据操作,即 cellContents = n;Console.WriteLine("Produce: {0}", cellContents);
同时设置readerFlag为真,并通知等待队列中的consumer线程。readerFlag = true; Monitor.Pulse(this); consumer线程则执行Wait(),等待producer线程执行完后,将锁再次归还consumer线程。
如果为true,即有数据可以读取,producer线程执行Wait(),等待consumer线程执行完后,将锁再次归还producer线程;consumer线程执行读数据操作,即Console.WriteLine("Consume: {0}", cellContents); 同时设置readerFlag为假,并通知等待队列中的producer线程。readerFlag = false;
Monitor.Pulse(this);