异步操作(二)

  当然笔者认为触及这方面知识就就必须对委托很了解,这方面编程会用到委托,同时还要清楚C#为委托提供的语法便利,笔者发现异步操作委托真是“绝配”了。笔者个人认为异步编程也会是将来的趋势,因为异步的程序(不能说绝对,但是大多数)效率还是比较高的。我们同时也在享受异步操作带来的优势,WEB服务器环境就是一个异步环境,每一个请求都是独立的线程。很难想象只能同步处理一个请求的WEB服务器有什么用?

  定期执行受计算限制的异步操作

      System.Threadding命名空间中定义了一个Timer类,可以使用这个类让CLR定期地调用方法。

public sealed class Timer:MarshalByrefObject,IDisposable
{
public Timer(TimerCallback callback,Object state,Int32 dueTime,Int32 period);
public Timer(TimerCallback callback,Object state,UInt32 dueTime,Int32 period);
public Timer(TimerCallback callback,Object state,Int64 dueTime,Int64 period);
public Timer(TimerCallback callback,Object state,Timespan dueTime,Timespan period);
}

  参数callback用来标识希望线程池中的线程回调方法。回调线程必须与System.Threading.TimerCallback委托类型匹配。构造器state参数允许我们将状态数据传递给回调方法,如果没有状态数据可传递,可以传递null。可以使用dueTimer参数来告诉CLR在第一次调用回调方法时需要等待多少毫秒。如果希望回调方法立即被调用,可以将dueTime参数设置成null。最后一个参数允许我们指定回调方法被调用的时间间隔。如果该参数传递值为Timeout.Infinite(-1),那么线程池中的线程只会调用回调方法一次。

所有的Timer对象到达后,CLR线程就会醒来,并且在其内部调用ThreadPool的QueueUserWorkItem方法将一个条目加入到线程池的队列中,从而导致回调方法被调用。如果回调方法需要较长时间来执行,那么定时器可能会再一次触发。这可能到设置多个线程池中的对象同时执行回调方法。那么我们需要增加一些线程同步锁来防止数据被破坏。

Timer类还提供了Change和Dispose方法,前者允许我们更改或者重新设置Timer对象的启动时间和间隔,后者是取消定时器,而且可以在所有挂起的回调方法完成时选择性的通知otifyObject参数标识的内核对象

     给出一个例子,线程池立即开始调用方法,然后每隔2秒调用一次方法。

using System;
using System.Threading;

public static class Program
{
public static void Main()
{
Console.WriteLine(“Main thread:statting a timer”);
Timer t
=new Timer(ComputeBoundOp,5,0,2000);
Console.WriteLine(“Main thread:Doing other work here”);
Thread.Sleep(
10000);
//取消定时器
t.Dispose();
}

//该方法由线程执行
private static void ComputeBoundOp(Object state)
{
Console.WriteLine(“In ComputeBoundOp:State
={0}”,state);
Thread.Sleep(
1000);
}
}

  异步编程模式APM

      APM能够让我们更好的执行异步操作,同时在FCL中有许多类型都支持它

  • FileStream 操作:BeginRead 、BeginWrite 。
  • DNS 操作:BeginGetHostByName 、BeginResolve 。
  • Socket 操作:BeginAccept 、BeginConnect 、BeginReceive 等等。
  • WebRequest 操作:BeginGetRequestStream 、BeginGetResponse 。
  • SqlCommand 操作:BeginExecuteReader 、BeginExecuteNonQuery 等等。这可能是开发一个Web 应用时最常用的异步操作。
  • WebServcie 调用操作: 例如.NET 2.0 或WCF 生成的Web Service Proxy 中的BeginXXX 方法、WCF 中ClientBase<TChannel> 的InvokeAsync 方法。

  使用ARM执行受I/O限制的异步操作

  执行异步操作时构建高性能,可扩展性应用程序的关键,它允许我们能够用非常少的线程来执行许多操作。加上线程池,异步操作允许我们利用机器中的所有CPU。当然这里面存在许多问题,因此设计了异步编程模式(APM),让开发人员方便的利用这种能力。

       ARM一个主要特征就是它提供了三个聚集技巧。回想下使用ThreadPool的QueueUserWorkItem方法时,CLR没有内置方法来供我们查找异步操作何时完成。CLR也没有内置方法来发现异步操作的结果,也没有内置方法让线程池中的线程汇报结果。ARM提供了三种机制,利用这三种机制,我们可以判断异步操作时什么时候完成的,而且这三个机制允许我们获得一步操作的结果。

假定希望使用ARM从一个文件流中异步地读取一些字节。首先要调用System.IO.FileStream对象的构造函数并接受一个System.IO.FileOption参数来构建一个System.IO.FileStream对象

  对于System.IO.FileOptions参数来说,我们传递一个FileOption.Asynchronous标记,该标记告诉FileStream对象我们准备在文件上执行异步读/写操作。为了从FileStream对象中同步的读取字节,我们可以调用它的Read方法,该方法的原型如下:

public Int32 Read(Byte[] array,Int32 offset,Int32 count)

该方法相信大家已经很熟悉了,这里就不多介绍了,但是笔者这里要提醒一点直到读取所有的字节都已经放到Byte类型数组中,方法才会返回。同步I/O操作效率很低,因为I/O操作的定时很难预测的,并且在等待I/O操作完成时,调用线程被挂起,因此它不能再做其他任何工作,从而浪费资源。如果Windows操作系统已经缓存了文件数据,该方法几乎可以立即返回。但是如果数据没有缓存,那么Windows 就不得已与磁盘驱动器硬件进行通信来加载磁盘中的数据。甚至可能要跨网络与服务器通信,让服务器与它的磁盘驱动器通信得以返回数据。因此这里我们可以从文件中异步地读取字节,可以调用FileStream的BeginRead方法:

IAsynResult BeginRead(Byte[] array,Int32 offset,
Int32 numBytes,AsynCallback userCallback,Object stateObject)

BeginRead方法前三个参数与Read方法参数相同。后面两个参数以后会提到的。BeginRead方法实际上讲请求加入到Windows设备驱动程序的队列中,而Windows的设备驱动程序知道如何与正确的硬件设备通信。就这样硬件接管了该操作。

BeginRead方法返回一个其类型实现了System.IAsyncResult接口的对象引用。调用该方法时,它构建一个对象来唯一的标识I/O操作请求,并将请求加入Windows设备驱动程序队列,然后将IAsyncResult对象返回给我们。我们可以将IAsyncResult对象看做收据。

       事实上数据中是否已经包含了所请求的数据,因为I/O操作已经被异步地执行了。我们不知道什么时候得到数据,所以我们需要得到一种方法来发现结果,并且知道什么时候检测到的结果。上述情况称为异步操作结果的聚集。

APM的三个聚集技巧

1.APM的等待直至完成聚集技巧

  为了启动一个异步操作,我们可以调用一些BeginXxx方法。所有这些方法都会将请求操作排队,然后返回一个IAsyncResult对象来标识挂起的操作。为了获得操作的结果,我们可以以IAsyncResult对象为参数调用相应的EndXxx方法。根据记录,所有的EndXxx方法都接受一个IAsyncResult对象作为它的一个参数。在调用EndXxx方法时,我们是在请求的CLR返回由IAsyncResult对象标识的异步操作的结果。如果异步操作已经完成,那么调用EndXxx方法时,它将立即返回结果。另一方面如果异步操作没有完成,EndXxx方法将挂起调用线程直至异步操作完成,然后返回结果。

下面重新考虑从FileStream对象中读取字节的范例

using System;
using System.IO;
using System.Threading;

public static class Program
{
public static void Main()
{
//打开指示异步I/O操作文件
FileStream fs=new FileStream(@”C:\Boot.ini”,FileMode.Open,FileAccess.Read,
FileShare.Read,
1024,FileOptions.Asynchronous);

Byte[] data
=new Byte[100];

//为FileStream对象初始化一个异步读操作
IAsyncResult ar=fs.BeginRead(data,0,data.Length,null,null);

//执行一些代码


//挂起该线程直至异步操作结束并获得结果
Int32 bytesRead=fs.EndRead(ar);

fs.Close();

//此时可以确定已经读取了数据了(但是结果都是一些16进制的数据)

}
}

上面的代码,并没有有效地利用ARM。该程序在调用一个BeginXxx方法之后立即调用了一个EndXxx方法,这样做了,调用线程进入睡眠状态,在等待操作的完成。如果希望异步执行该操作,可以调用一个Read方法。但是如果在BeginRead和EndRead操作之间放一些代码,会看到ARM一些价值,因为这些代码可以在读取文件字节的过程中执行。下面的代码对前面的程序做了实质性的修改。新版本的程序同时从多个流中读取数据。

private static void ReadMultipleFiles(params String[] pathname)
{
AsyncStreamRead[] asrs
=new AsyncSreamRead[pathnames.Length];

for(Int32 n=0;n<pathnames.Length;n++)
{
//打开指示异步I/O操作文件
Sream stream=new FileStream(pathnames[n],FileMode.Open,FileAccess.Read,
FileShare.Read,
1024,FileOptions.Asynchronous);

//为Stream 对象初始化一个异步操作
asrs[n]=new AsyncStreamRead(stream,100);
}

//所有的流都已经打开,而且所有的读请求都已经排队,它们都同时并发执行

//下面获取并显示结果
for(Int32 n=0;n<asrs.Length;n++)
{
Byte[] bytesRead
=asrs[n].EndRead();

//显示结果
}
}

private sealed class AsyncStreamRead
{
private Sream m_stream;
private IAsyncResult m_ar;
private Byte[] m_data;

public AsyncStreamRead(Stream steram,Int32 numBytes)
{
m_stream
=stream;
m_data
=new Byte[numBytes];

//为Stream对象初始化一个异步操作
m_ar=stream.BeginRead(m_data,0,numByte,null,null);
}

public Byte[] EndRead()
{
//挂起该线程直至获得结果
Int32 numBytesRead=m_stream.EndRead(m_ar);

//已经没有操作执行任务,关闭流
m_stream.Close();

//调整数组大小节省空间
Array.Resize(ref m_data,numBytesRead);

return m_data;
}
}

上代码同时执行所有读操作时,将非常有效。但同时存在低效的地方。ReadMultipleFiles方法按照请求生成次序为每个流调用EndRead方法。这种方式效率不高,因为不用的流需要不同的时间来读取数据。这里完全由可能第二个流中的数据会在第一个流中的数据之前读取完成。所以理想情况下,发生了上述情况,应该首先处理第二个流中的数据。

 

本文知识来源:《CLR Via C#》

posted @ 2010-05-07 11:16  胡佳180815  阅读(2850)  评论(0编辑  收藏  举报