单向操作特别适用于"触发然后忘记"场景,在该场景中,客户端程序并不期望服务回传任何信息。但是,许多操作并不适用于这种情况,其向客户端程序返回数据。为了处理这些情况,WCF支持异步操作和IAsyncResult设计模式。在WCF中你可以使用两种方式实现IAsyncResult设计模式:在客户端 程序中异步调用操作;或在WCF服务中实现异步操作。
IAsyncResult 设计模式并不是为WCF特别设计的,它在.NET Framework中被广泛使用。更多详细信息,可以查阅MSDN的"异步编程设计模式" http://msdn.microsoft.com/en-us/library/ms228969.aspx
在客户端程序中异步调用操作
使用WCF,你可以生成一个特定版本的代理类,使用该代理类客户端程序可以异步地调用服务的操作。在使用svcutil命令行工具时指定"/aysnc"标记可以生成特定版本的代理类。你也可以在添加服务引用向导时,点击高级按钮,然后在服务引用设置对话框中选择生成异步操作复选框以生成该特定版本的代理 类。
一个异步代理类为每个操作提供一对begin和end方法。客户端程序可以调用begin方法以实例化该操作。Begin方法在发送完请求后理解返回,但是.NET Framework运行时会在客户端创建一个新的线程用以等待请求的响应。当你调用begin方法,你需提供回调方法的名字。当服务完成操作并返回结果只客户端 代理后,新线程中的回调方法开始执行。你使用回调方法获取服务的返回结果。你应该调用操作的end方法以表明你已处理完服务的响应。
理解随后这点非常重要:若要服务支持上述异步编程,你并不需要采用任何方式修改该服务。实际上,服务自身都不必为一个WCF服务;它可能是通过其他技术实现的服务。使操作对客户端程序为异步操作的代码封装在客户端的代理对象和.NET Framework运行时中。所有与线程相关的问题均由运行在客户端的WCF运行时中的代码去处理。服务所关注的是其操作与之前章节中的练习一样被同步地调 用。
在WCF服务中实现异步操作
使用WCF,你还可以在服务中实现异步操作。在这种情况下,
服务提供自己的一对begin和end方法组成该操作。客户端程序通过代理对象调用操作使用普通的操作名调用操作(不使用begin方法)。WCF运行时分配操作的请求至begin方法,因此客户端程序不必关注服务是否采用异步方式实现该操作。
服务的开发人员可以添加逻辑至begin方法以选择操作应同步或地者异步地执行。比如,如果服务当前的负载比较小,那么可以使用同步地方式执行操作以使该操作可以尽快的完成。如果负载增加,服务可能选择以采用异步的方式执行操作。通过这种方式实现操作不需要修改客户端程序。如果该操作经过很长一段时间后才向 客户端返回数据,那么你应该使用这种方式实现这些操作。
当定义服务的一个操作时,在该操作上设置OperationContract特性类的AysncPattern属性为true,以指定该操作支持异步处理;然后提供一对遵循前面所述的名称转化和签名的方法以实现IAsyncResult设计模式。
在下面一组练习中,你将添加另外一个名为CalculateTotalValueOfStock操作至AdventureWorksAdmin服务。该操作的目的是确定AdventureWorks仓库中所有存货当前的总价值。该操作将耗费一段时间才会运算完成,因此你需要采用异步方法实现该操作。
练习:添加一个异步操作至AdventureWorksAdmin服务
1. 在Visual Studio中,打开App_Code文件夹下的IService.cs文件。添加下面的操作至服务合约AdventureWorksAdmin
该操作包含两个方法BeginCalculateTotalValueOfStock和EndCalculateTotalValueOfStock。它们一起组成单个名为CalculateTotalValueOfStock的异步操作。在该操作中你必须使用上述命名方式来命名这两个方法,只有这样客户端代理对象 才可以正确地识别它们。你可以任意指定该操作begin方法所需的参数(在本例中,客户端程序将传递一个字符串参数以识别操作的调用),但是该方法的最后两个参数必须为一个AysncCallback对象,该对象将指向客户端程序中的一个回调方法;以及另外一个为参数为object,它持有客户端程序提供的状 态信息。Begin方法的返回值必须为IAsyncResult。 End方法必须接收单个类型为IAsyncResult的参数,但是其返回值应当对应操作的返回值。在本例中, CalculateTotalValueOfStock操作返回一个int类型,其值为存货总价值的计算结果。
该操作的另外一个重要部分是OperationContract特性类的AsyncPattern属性。你只需要对begin方法应用 OperationContract特性。当你生成该服务的元数据时(比如生成客户端代理),该属性将导致begin和end被认为是单个异步操作的实 现。
2. 在解决方案窗口,在App_Code文件夹上点击右键,然后选择添加已存项目。添加AyncResult.cs文件,该文件位于D:\Works\Solutions\WCF\Step.by.Step\Chapter12目录下。
3. 打开AysncResult.cs文件并检查其内容。它包含一个generic类,该类的名字为AsyncResult,它实现了 IAsyncResult接口。该类更详细的讨论超出本书的返回,但是AysncResult类的目的在于为其他实现异步方法的类提供同步方法和状态信 息。
在本练习中,该类的两个重要成员是
- Data属性可以读取异步操作的返回的数据。在本列中,CalculateTotalValueOfStock操作将填充该属性,并在执行end方法时返回AsyncResult至客户端程序。
- AysncResult构造方法接收两个参数,并将这两个参数存贮在对应的两个私有变量中。服务将使用synchronous参数指明是否同步地调用操作,stateData参数将指向传入begin方法的最后一个参数对象(保存该对象非常重要,因为该对象必须返回值客户端程序以确保该对象完成了处理)
4. 打开App_Code文件夹的Service.cs文件。然后添加如下的代理至AdventureWorksAdmin类
你将在后续添加的方法中使用该代理。
5. 添加如下的方法至AdventureWorksAdmin类
上述方法工作的细节超出本书的讨论范围(严格地讲,该方法与WCF没有任何关系)。但是总的来说,该方法生成一个随机数,如果该数为偶数,它将使用同步方式执行操作(模拟低负载场景);否则使用异步方式执行操作(模拟高负载场景)。在同步场景下,代码将创建一个新的AsyncResult对象,线程休眠20 秒以模拟执行计算的时间,然后用数字55555555填充AysncResult对象。在异步场景中,代码同样创建一个新的AsyncResult对象,但会产生一个新的线程,该新线程在后台休眠30秒。代码并不填充AsyncResult对象因为当后台休眠的线程苏醒后将填充AysncResult对象。 在上述两种情况下,
代码都会调用客户端程序中的回调方法,传递AsyncResult对象为回调方法的参数。客户端程序将从该对象中接收到计算的结果。回调方法方法将返回相同的AysncResult对象(这是IAsyncResult设计模式的要求)。
该方法还显示消息对话框以帮助你追踪方法的执行,并确定操作是同步还是异步的。
6. 添加end方法至AdventureWorksAdmin类
当begin方法完成后调用上述end方法。该方法的目的是从传入的IAsyncResult对象中获取Data属性的值(该值就是操作的计算结果)。如果操作以异步方式执行,那么操作可能还未完成(调用一个异步操作的begin方法的程序可以在begin方法结束后的任何时间调用end方法,因此end方法必须确保在其返回之前操作已经完成)。在本例中,在AsyncResult对象指明操作已经结束前,该方法将一直处于等待状态。只有操作结束后,该方法才从 AsyncResult中提起数据并返回该数据。
7. 添加如下方法方法至AdventureWorksAdmin类
如果begin方法决定异步地执行任务,那么begin方法将通过在后台创建一个新线程模拟执行计算,新线程将休眠30秒。当后台线程开始休眠时,注册EndAysncSleep方法为一个回调方法。30秒之后,操作系统激活后台线程并调用该方法。该方法填充AysncResult对象的Data属性, 然后指明操作现在已经完成。这将释放服务的主线程,因为主线程在等待end方法,end方法结束后允许服务的主线程向客户端返回数据。
请注意,操作的返回值是不同的,这取决于服务执行同步操作(返回55555555)还是异步操作(返回999999)。
8. 生成解决方案。
练习:在WCF客户端程序中调用CalculateTotalValueOfStock操作
1. 在AdventureWokrsAdminTestClient项目中,展开服务引用文件夹,然后在AdventureWorksAdmin服务引用上点击右键,然后点击更新服务引用。
该操作将生产一个新版本的客户端代理,该代理包含CalculateTotalValueOfStock操作
2. 在解决方案浏览器窗口,确保显示所有文件复选框被选中。
3. 展开AdventureWorksAdmin服务引用,展开Reference.svcmap文件夹,然后打开Reference.cs文件。检查 AdministrativeService接口中的服务合约的定义。请注意名为CalculateTotalValueOfStock新操作,你会发现 该实现该操作的方法并没有标记为begin或end。实际上,该操作以异步地方式实现,并对客户端程序完全是透明的。
4. 编辑Program.cs文件,删除try代码片段中调用GenerateDailySalesReport操作和Console.WriteLine语句。然后添加下面代码
上述代码三次调用CalculateTotalValueOfStock方法并显示调用的结果。如果顺利,服务这些调用中至少有一个与其他两个以不同的方式执行(要么同步要么异步)。
5. 在非调适模式下运行解决方案
接下来将要发生的结果取决于服务生成的随机数,该随机数决定操作是同步地执行还是异步地执行。如果你不幸运,你必须等待20秒才会看到第一个消息提示框:
这是因为CalculateTotalValueOfStock方法中生成的随机数产生了一个偶数,然后将同步地执行操作。该消息提示框之后立即出现下面的消息提示框:
当你点击确认按钮后,你将看到结果55555555显示在客户端程序的控制台窗口中。
如果你只看到第二个消息提示框,那么BeginCalculateTotalValueOfStock已经决定执行异步地方法。当你关闭消息提示框后,你需要等待30秒,你将会看到下面的消息提示框出现:
999999也将显示在客户端程序的控制台窗口中。客户端程序调用CalculateTotalValueOfStock操作的每个方法都将重复上述过程。
6. 在客户端程序的控制台窗口中按ENTER键停止客户端。
到目前为止,一切都相当好。你已经解决了许多麻烦, 这样可以允许服务决定最好的策略执行长时间或者耗费资源的造作。但糟糕的是,到目前为止,客户端都的一切仍旧使用同步方式;每次调用 CalculateTotalValueOfStock操作都将被阻塞直到该操作完成。幸运的是,你可以通过在使用svcutil工具是添加/async 参数生成支持客户端异步操作的代理。这就是下面练习需要完成的。
练习:在WCF客户端程序中异步调用CalculateTotalValueOfStock操作
1. 在AdventureWokrsAdminTestClient项目中,展开服务引用文件夹,在AdventureWorksAdmin服务引用上点击右键,然后点击配置服务引用。在服务引用设置对话框中,选择生成异步操作复选框,然后点击确认按钮
2. 检查Reference.svcmap文件夹下的Reference.cs文件。
你将看到客户端代理现在对服务合约的两个操作都包含了begin和end方法,因为你可以异步地调用它们(每个操作的同步版本的方法也包含在代理中)。这些改变仅由客户端代理实现;服务实际上并不关注它们。
3. 编辑AdventureWokrsAdminTestClient项目下的Program.cs文件。移除调用CalculateTotalValueOfStock,Console.WriteLine以及关闭代理等语句。然后添加下列代码:
上述代码三次调用客户端的异步版本的CalculateTotalValueOfStock方法。结果将在一个名为 CalculateTotalValueOfStockCallback的方法中处理,该方法将在下一步中添加。Proxy对象作为状态参数传入 CalculateTotalValueOfStockCallback方法。
移除proxy.Close方法非常重要。如果你此时关闭代理,WCF运行时将在异步调用完成之前销毁客户端的通道堆栈,那么客户端程序将不能获取服务的相应。
4. 添加客户端的回调方法
该方法是一个回调方法。当CalculateTotalValueOfStock操作完成,代理将执行该方法。它从服务的回传中获取对象(该对象为状态对 象,其为proxy的一个引用,该引用在客户端程序调用BeginCalculateTotalValueOfStock方法时作为第三个参数传入 BeginCalculateTotalValueOfStock方法中),该回调方法还使用该对象调用 EndCalculateTotalValueOfStock方法。End方法的返回值就是来自服务计算的存货总价值。
5. 在非调适模式下运行解决方案。
客户端程序开始然后立即显示"Press ENTER to finish"。这是因为调用BeginCalculateTotalValueOfStock方法将不再阻塞客户端程序。
现在不要按ENTER键;允许客户端程序继续执行。在20秒或者30秒之后,你将看到上一个练习中的消息提示框,该消息提示框指明服务同步地还是异步地执行每个请求。当所有操作完成后,计算结果将出现在客户端程序控制台窗口中。
6. 当所有结果都显示后,按ENTER键关闭客户端程序控制台窗口。
从这些练习中,你现在应该已经理解了在客户端异步调用操作和在服务总实现支持异步操作的不同。开发人员可以决定是否使用一对方法实现一个操作以实现IAsyncResult设计模式以独立于任何客户端程序。这些方法以单个方法出现在客户端程序,并且其实现是完全透明的。
相似地,当创建一个WCF客户端程序,开发人员希望调用异步地操作时,仅仅需要生产一个异步代理(要么使用添加服务引用向导,要么使用svcutil工具是添加/async参数)。客户端程序是否同步地调用操作对于服务是透明的。
最后,你应该认识到尽管客户端程序可以异地调用服务的操作,服务仍然可以选择使用同步方式实现操作;反之亦反。客户端和服务其相应的实现部分是相当灵活的。
更进一步,你还可以按照下列方式对服务的操作既定义同步版本又定义异步版本。
但是,如果你这么做,两个操作都将出现在服务的WSDL描述的同一个动作中。在这种情况下,WCF将不会抛出异常,并且比起异步版本的操作WCF将始终使用 同步版本的操作(因为WCF假设同步操作版本可以更快的获得输出)。因此,不要在同一个服务中对同一个操作既定义同步版本又定义异步版本。