还必须注意运行多个线程时的一些问题。它们可以同时运行,但如果线程访问相同的数据,就很容易出问题。必须实现同步机制。
创建线程的一种简单方式是定义一个委托,并异步调用它。委托是方法的类型安全的引用。Delegate类还支持异步地调用方法。在后台Delegate类会创建一个执行任务的线程。
使用不同的技术异步地调用委托,并返回结果。
1.投票
检查委托是否完成了它的任务。
Delegate类提供了BeginInvoke()方法,在该方法中,可以传递用委托类型定义的输入参数。
BeginInvoke()方法总是有AsyncCdback和object类型的两个额外参数(稍后讨论)。现在重要的是BeginInvoke()
方法的返回类型:IAsyncResult。通过IAsyncResdt,可以获得该委托的相关信息,并验证该委托是否完成了任务,
这是IsCompleted属性的功劳。
EndInvoke()用于返回委托方法的执行结果,EndInvoke()方法会一直等到委托完成其任务为止。
AsyncResult ar = dl.BeginInvoke(1,3000,null,null);
while(!ar.IsCompleted)
{
Console.WriteLine(".");
Thread.Sleep(50);
}
while (true)
{
Console.WriteLine(".");
if(ar.AsyncWaitHandle.WaitOne(50,false))
{
Console.WriteLine("CAn Get TheResult");
break;
}
}
int result = dl.EndInvoke(ar);
Console.WriteLine(result);
2.等待句柄
使用AsyncWaitHandle属性可以访问等待句柄。这个属性返回一个WaitHandle类型的对象,它可以等待委托线程完成其任务。而WaitOne()方法将一个超时时间作为可选的第一个参数,在其中可以定义要等待的最长时间。这里设置为5O毫秒。如果发生超时, WaitOne()方法就返回false.同样使用委托的EndInvokeo方法接收结果。
AsyncResult ar = dl.BeginInvoke(1,3000,null,null);
if (ar.AsyncWaitHandle.WaitOne(50,false))
{
Console.WriteLine("CAn Get TheResult");
break;
}
int result = dl.EndInvoke(ar);
Console.WriteLine(result);
3.异步回调
在BeginInvoke()方法的第三个参数中,可以传递一个满足AsyncCallback委托的需求方法。AsyncCauback委托定义了一个IAsyncResult类型的参数,其返回类型是void。 对于BeginInvoke()方法最后一个参数,可以传递任意对象,以便从回调方法中访问他。
传递委托实例很有用,这样回调方法就可以使用它获得异步方法的结果。
只要委托方法完成任务,就调用回调方法。不需要在主线程中等待结果。但是在委托线程的任务未完成之前,不能停止主线程,除非主线程结束时停止的委托线程没有问题
dl.BeginInvoke(1,3000,TakesAWhileCompleted,dl);
for(…..)
{
….
…
}
..
Static void TakesAWhileCompleted(IAsyncResult ar)
{
…
}
使用的回调方法是从委托线程中调用,而不是从主线程中调用。
除了定义一个单独的方法,并给它传递BeginInvike()方法之外,Lambda表达式也非常适合这种情况。参数ar是IAsyncResult类型。在实现代码中,不需要把一个值赋予BeginInvike()方法的最后ˉ个参数,因为Lambda表达式可以直接访问该作用域外部的变量d1。但是,Lambda表达式的实现代码仍是从委托线程中调用,以这种方式定义方法时,这不是很明显。
异步委托不仅能用于委托,相同的编程模型(即异步模式)在NetFmmewOrk的各个地方都能见到。例如,可以用HupWebRequest类的BeginGetResponse异步发送HttpWeb请求,使用sqlCommand类的BeginExecuteReader()方法给数据库发送异步请求。这些参数类似于委托的BeginInvoke()方法的参数,也可以使用相同的方式获得结果。(这些内容后面会提到)
staticvoid Main(string[] args)
{
var t =new Thread(ThreadMain);
t.Start();
Console.WriteLine("mainthread");
Console.ReadKey();
}
staticvoid ThreadMain()
{
Console.WriteLine("Running inThread");
Thread类的构造函数重载为接受Theadstart和ParameterizedThreadStart类型的委托参数。Threadstart委托定义了一个返回类型为void的无参数方法。在创建了Thread对象后,就可以用Start()方法启动线程.
给线程传递数据
给线程传递一些数据可以采用两种方式。一种方式是使用带Parameterizedhreadstart委托参数的Thread构造函数,另一种方式是创建一个自定义类,把线程的方法定义为实例方法,这样就可以初始化实例的数据,之后启动线程。
1.要给线程传递数据,需要某个存储数据的类或结构。这里定义了包含字符串的Data结构,但可以传递任意对象。
public struct Data
{
Publicstring Message;
}
如果使用了Parameterizedhreadstart委托,线程的入口点必须有一个object类型的参数,且返回类型为void。对象可以强制转换为任意数据类型.
Static void ThreadMain(object o)
{
Data d = (Data)o;
…
}
通过Thread类的构造函数,可以将新的入口点赋予ThreadMain,传递变量d,以此调用start( )方法。
Static void Main()
{
Var d = new Data{Message = “infro”;
Var t = new Thread(ThreadMain);
T.Start(d);
}
2.给新线程传递数据的另一种方式是定义一个类(参见MyThread类),在其中定义需要的字段,将线程的主方法定义为类的一个实例方法:
Public class MyThread
{
Privatestring data;
Public MyThread(string data)
{
This.data= data;
}
Public void ThreadMain()
{
Console.writeLine(data);
}
}
Static void Main()
{
Var d = new MyThread ( “infro”);
Var t = newThread(d.Thread.Main );
T.Start();
}
后台线程
只要有一个前台线程在运行,应用程序的进程就在运行。如果多个前台线程在运行,而main()方法结束了,应用程序的进程就仍然是激活的,直到所有前台线程完成其任务为止。
在默认情况下,用Thread类创建的线程是前台线程。线程池中的线程总是后台线程。
在用Thread类创建线程时,可以设置IsBackground属性,以确定该线程是前台线程还是后台线程。
Main()方法将线程t1的IsBackgromd属性设置为false(默认值)。在启动新线程后,主线程就把结束消息写入控制台中。新线程会写入启动消息和结束消息,在这个过程中它要睡眠3秒。在新线程会完成其工作前,这3秒钟有利于主线程结束。
classProgram
{
static void Main(string[] args)
{
var t1 = new Thread(ThreadMain) { Name ="MyThread",IsBackground =false};
t1.Start();
Console.WriteLine("Main threadis ending");
}
static void ThreadMain()
{
Console.WriteLine("Thread {0}started",Thread.CurrentThread.Name);
Thread.Sleep(3000);
Console.WriteLine("Thread {0}completed",Thread.CurrentThread.Name);
Console.ReadKey();
}
}
尽管主线程会提前完成其工作,但在启动应用程序时,会看到写入控制台的完成消息。原因是新线程也是一个前台线程。
线程的优先级
线程由操作系统调度。给线程指定优先级,就可以影响调度顺序。
在改变优先级之前,必须理解线程调度器。操作系统根据优先级来调度线程。调度优先级最高的线程以在CPU上运行。
线程如果在等待资源,它就会停止运行,并释放CPU。线程必须等待有几个原因,例如,响应睡眠指令、等待磁盘I/o的完成,等待网络包的到达等。
如果线程不是主动释放CPU,线程调度器就会抢占该线程。如果线程有一个时间量,它就可以继续使用CPU。
如果优先级相同的多个线程等待使用CPU,线程调度器就会使用一个循环调度规则,将CPU逐个交给线程使用。如果线程被其他线程抢占,它就会排在队列的最后:只有优先级相同的多个线程在运行,才用得上时间量和循环规则。
优先级是动态的。如果线程是CPU密集型的(一直需要CPU,且不等待资阏,其优先级就低于用该线程定义的基本优先级。如果线程在等待资源,随着优先级的提高它的优先级就会增加。
由于优先级的提高,线程才有可能在下次等待结束时获得CPU。
在Thread类中,可以设置Priority属性,以影响线程的基本优先级。Priority属性需要ThreadPriority枚举定义的一个值。定义的级别有Hightest,AboveNormal,BelowNormal和Lowest.
控制线程
调用Thread对象的Start()方法,可以创建线程。但是,在调用Start()方法后,新线程仍不是处于Running状态,而是处于Unstarted状态。只要操作系统的线程调度器选择了要运行的线程,线程就会改为Runing状态。读取Thread.ThreadState属性,就可以获得线程的当前状态。
使用Thread.Sleep()方法,会使线程处于WaitSleepJoin状态,在经历Sleep()方法定义的时间段后,线程就会等待再次被唤醒。
要停止另一个线程,可以调用Thread.Abort()方法。调用这个方法时,会在接到终止命令的线程中抛出一 个ThreadAbortException类型的异常。用一个处理程序捕获这个异常,线程可以在结束前完成一些清理工作。
线程还可以在接收到调用Thread.RestAbort()方法的结果ThreadAbortException异常后继续运行。
如果线程没有重置终止,接收到终止请求的线程的状态就从AbortRequested改为Aborted。
如果需要等待线程的结束,就可以调用Thread.Join()方法。Thread.Join()方法会停止当前线程,并把它设置为waitSleepJoin状态,直到加入的线程完成为止。
创建线程需要时间。如果有不同的小任务要完成,就可以事先创建许多线程,· 在应完成这些任务时发出请求。这个线程数最好在需要更多的线程时增加,在需要释放资源时减少。
不需要自己创建这样一个列表。该列表由ThreadPool类托管。这个类会在需要时增减池中线程的线程数,直到最大的线程数。
池中的最大线程数是可配置的。在双核CPU中,默认设置为1023个工作线程和1000个I/o线程。也可以指定在创建线程池时应立即启动的最小线程数,以及线程池 中可用的最大线程数。
如果有更多的作业要处理,线程池中线程的个数也到了极限,最新的作业就要排队,且必须等待线程完成其任务。
示例应用程序首先要读取工作线程和I/o线程的最大线程数,把这些信息写入控制台中。
接着在for循环中,调用ThreadPool.QueueuserWorkTtem()方法,传递一个WaitCallBack类型的委托,把JobForATheadO方法赋予线程池中的线程。线程池收到这个请求后,就会从池中选择一个线程,来调用该方法。如果线程池还没有运行,就会创建一个线程池,并启动第一个线程。如果线程池己经在运行,且有一个空闲线程来完成该任务,就把该作业传递给这个线程。
线程池使用起来很简单,但它有一些限制:
1.线程池中的所有线程都是后台线程。如果进程的所有前台线程都结束了,所有的后台线程就会停止。不能把入池的线程改为前台线程。
2.不能给入池的线程设置优先级或名称。
3.对于COM对象,入池的所有线程都是多线程单元(MTA)线程。许多CoM对象都需要单线程单元线程。
4.入池的线程只能用于时间较短的任务。如果线程要一直运行(如Word的拼写检查器线程),就应使用Thread类创建一个线程。