第五讲:异步操作

代码

https://yunpan.cn/cPns5DkGnRGNs   密码:3913

 


 

我们接着上一张的内容去讲,上一张的最后,看到了我们的程序报错了。 

这里我们解释一下为什么报错,为什么会出现死锁呢?

异常:究其本质,这是一个死锁导致的异常,由于默认的情况是服务按Single并发模式执行(在服务器执行过程中,服务对象只能被一个线程访问。WCF通过加锁的机制保证服务对象的独占性使用,也就是说在服务执行开始时会对服务对象加锁,该锁在服务操作结束之后释放)

 

在Add操作执行的过程中,服务端会回调客户端操作进行运算结果的显示工作。

如果回调采用单向操作:

回调请求一经发出便会立即返回,不需要等待服务端的反馈,操作可以继续得到执行,直到操作正常结束。而另一方面的服务端,当调用操作在客户端正常执行后,需要反馈回到服务端所以试图访问操作的时候,发现对象被服务操作执行的线程锁定,所以他会等待服务操作执行完成后将锁释放。这样,服务操作需要等待回调操作进行正常的返回以便执行后续操作,而回调操作只有等待服务操作执行完成之后将锁释放才可以访问。所以就造成了死锁状态。

通俗讲就是  因为客户端第一次调用服务端的Add方法的线程正在执行并且执行到了等待客户端的反馈信息,就这样一直的等待,资源不会被释放以至于处于锁定状态,而客户端也执行完成了相关操作,需要进行反馈服务端信息,但是服务端一直处于锁定状态,而无法再次请求。两边就产生了 矛盾。所以死锁了。

 


 

 

那么怎么解决上面的问题呢?

 

使用多线程或者异步操作

 

多线程与异步操作

按照操作执行所需要的资源类型,我们可以将操作分为CPU绑定型操作和I/O操作。对于前者,操作的执行主要利用CPU进行密集的计算:而对于后者,大部分的操作处理时间花在I/O操作处理,比如访问数据库,文件系统,网络资源等。对于I/O的操作,我们可以充分利用多线程的机制,让多个操作在自己的线程并发执行,从而提高系统性能和响应能力。服务调用就是典型的I/O操作,所以多线程在服务调用中具有广泛的应用。

 

如果按照异步操作发生的位置,可以将WCF应用的异步操作分为下面2种情况:

1:异步信道调用:

  客户端通过绑定创建的信道向服务端发送消息,从而实现了对服务的调用。客户端也可以通过代理对象异步地调用信道,从而实现异步服务调用。

2:单向(One-Way)消息交换   (这一种我们第四讲已经说过了,只是没有点透,这里说单向消息交换   与 第一种  异步信道  其实是  异曲同工 的效果   )

  客户端信道通过单向的消息交换模式向服务端发送消息,消息一旦抵达传输层马上返回,从而达到异步服务调用的效果。

 

 

 

 

我们重点说说  异步信道的调用:

为了方便客户端进行异步的服务调用,最简单的方式是通过添加服务引用的方式来创建(第一讲我们说过)。通过该方式来创建异步服务代理,创建的方式只需要在添加服务引用对话框中点击“高级”按钮,便会填出一个“服务引用设置”对话框,勾选“生成异步操作”复选框即可。

 

[ 5-01 ]

 

[ 5-02 ]

 

不管 勾不勾选 " 允许生成异步操作 " 的复选框   都会生成一个继承自ClientBase<TChannel>的类,但 勾选后 所不同的是,该类中会多出一些与异步服务调用相关的成员。 在具体通过服务代理进行异步服务调用的时候,可以采用不同的调用方式,不仅可以采用参数典型的BeginXxx和EndXxx的形式,也可以采用回调的形式,还可以采用事件注册的方式。

 

例如:我们的契约中有个Add的方法,如果勾选了 " 允许生成异步操作 "  的复选框  就会多出一个  BeginAdd  和   EndAdd  的方法,这两个方法就是异步操作的方法。

 


 

 

 

我们做一个异步服务调用的小Demo,代码还是在 云盘,自己去找:

 

说明一下,异步服务调用 的发起者 一定是 客户端,所以  其它地方 不需要额外说明,与之前一样不变的代码

 

[ 5-03 ]

 

 1 using System;
 2 using System.Collections.Generic;
 3 using System.Linq;
 4 using System.Text;
 5 using System.ServiceModel;
 6 
 7 
 8 namespace Client
 9 {
10     class Program
11     {
12         static void Main(string[] args)
13         {
14 
15 
16             //必须先通过 先  添加服务引用
17 
18             //通过BeginXxx/EndXxx进行异步服务调用
19             //在调用BeginAdd方法后,可以做一些额外的处理工作,这些工作将会和Add服务操作的调用并发地运行,最终的运算结果通过EndAdd方法得到
20 
21             //创建 服务引用的 代理
22             ServiceCalculator.CalculatorClient proxy = new Client.ServiceCalculator.CalculatorClient();
23             //异步操作
24             IAsyncResult asynResult = proxy.BeginAdd(1, 2, null, null);
25             //得到返回的结果
26             double result = proxy.EndAdd(asynResult);
27             proxy.Close();
28             Console.WriteLine("x+y={2} when x={0} and y={1}", 1, 2, result);
29             Console.Read();
30 
31 
32             /*
33              其实上面的方法并不好
34              是当EndAdd方法被执行的时候 如果异步执行的Add方法(也就是调用服务端的方法)还没有执行结束的话,在EndAdd方法这里将会阻塞当前线程(这个当前的线程指的是 客户端的线程)
35              并等待异步方法(调用服务端的Add方法)的结束,这样往往不能起到多线程并发执行应有的作用。我么真正希望的是在异步执行结束后自动回调设定的操作,这样就可以采用回调的方式
36              来实现这样的机制。
37              
38              去看第二种异步调用的方法
39              
40              */
41 
42 
43         }
44     }
45 }

 

上面的是第一种 异步 服务调用,但是不完善。有朋友问 不完善写这个弄啥嘞,这里只是让不太理解异步调用的朋友加深印象。

 

[ 5-04 ]

 

 1 using System;
 2 using System.Collections.Generic;
 3 using System.Linq;
 4 using System.Text;
 5 using System.ServiceModel;
 6 
 7 
 8 namespace Client
 9 {
10     class Program
11     {
12         static void Main(string[] args)
13         {
14             //通过回调的方式进行异步服务调用
15             //在异步执行结束后自动回调设定的操作,这样就可以采用回调的方式来实现这样的机制。
16 
17             //创建 服务引用的 代理
18             ServiceCalculator.CalculatorClient proxy = new Client.ServiceCalculator.CalculatorClient();
19             //执行Add的异步方法BeginAdd   1,2为参数,异步执行结束后自动调用的委托方法,向委托传入对象
20             proxy.BeginAdd(1, 2, delegate(IAsyncResult asyncResult)
21             {
22                 //这里得到的就是 向委托传入的对象  也就是 proxy.BeginAdd 的第四个参数 new double[] { 1, 2 }
23                 double[] operands = asyncResult.AsyncState as double[];
24                 //得到返回的结果
25                 double result = proxy.EndAdd(asyncResult);
26                 //关闭代理对象
27                 proxy.Close();
28                 Console.WriteLine("x+y={2} when x={0} and y={1}", operands[0], operands[1], result);
29             }, new double[] { 1, 2 });
30             Console.Read();
31         }
32     }
33 }

 

上面这种就真正的 能达到  异步线程的调用。  这种方式 是比较提倡使用的方式。

 

再说最后一种 异步服务调用的 方式:

 

[ 5-05 ]

 

 1 using System;
 2 using System.Collections.Generic;
 3 using System.Linq;
 4 using System.Text;
 5 using System.ServiceModel;
 6 
 7 
 8 namespace Client
 9 {
10     class Program
11     {
12         static void Main(string[] args)
13         {
14             //通过事件注册的方式进行异步服务调用
15             //实际上,事件注册和通过回调从表现上看比较类似。
16 
17             //创建 服务引用的 代理
18             ServiceCalculator.CalculatorClient proxy = new Client.ServiceCalculator.CalculatorClient();
19 
20             //异步执行结束后自动调用的事件( 也就是在 下面的 代码  proxy.AddAsync(1, 2, new double[] { 1, 2 })  完成之后 调用的事件  )
21             //第一个参数为他本身,第二个参数为它整个事件的数据源
22             proxy.AddCompleted += delegate(object sender, Client.ServiceCalculator.AddCompletedEventArgs argss)
23             {
24                 //获取 向事件传入的对象  也就是 proxy.AddAsync 的第三个参数 new double[] { 1, 2 }
25                 double[] operands = argss.UserState as double[];
26                 //得到返回的结果
27                 double result = argss.Result;
28                 //关闭代理对象
29                 proxy.Close();
30                 Console.WriteLine("x+y={2} when x={0} and y={1}", operands[0], operands[1], result);
31             };
32             proxy.AddAsync(1, 2, new double[] { 1, 2 });
33             Console.Read();
34         }
35     }
36 }

 

这种方式 其实  跟 第二种 方式  大差不差,所以 也是一个推介的 写法,至于 用那一种,大家自己看吧。第一种是不推介使用的。

 

 

到这里 就把  服务契约就说完了。

 


 

说到这里,我们不得不提一下 异步的 好坏

 

 

异步的优缺点及其应用场合

异步并不一定能提高系统性能,甚至因为线程的创建,消亡,和切换会增加系统开销,但异步除了提高性能,还可以增强系统的健壮性。在过去,windows程序总是单线程的,在这样的系统中,如果出现了异常,系统就会 因此而崩溃,甚至连我们的操作系统也是单线程的,所以每次出现异常,我们的计算机用户都要不厌其烦强制关机,然后重启才能解决问题。加入多线程之后,当一个线程上的任务发生异常的时候,其他线程有能力不受影响,从此防止整个应用程序的崩溃。此外如果用户是在一个UI中操作某项耗时的操作,如果不使用异步,那UI线程就会被阻塞,导致界面无法响应,用户就会很无助,增加了异步,让复杂的任务在另外的线程中完成,就会有比较好的用户体验。而且异步并不是说对性能提高没有作用,CLR线程的创建,销毁,和线程上下文切换的确会有很大的开销,比如每创建一个线程,都必须申请1MB的地址空间用于线程的用户模式,申请12KB左右的地址空间用于线程的内核模式,而且还要求进程调用每个dll中的一个 固定的函数来通知所有的dll系统创建了一个新的线程,同样在销毁的时候,也要做类似的通知,上面这一切似乎都说明了异步操作对于性能的坏处,但事实并非完全如此,我们知道当前的处理器基本上都是双核,或者支持hyper-thread,一个线程的执行总会占用1个cpu逻辑核,如果我们的计算机是4核,8核,而我们不采用异步,那其实多核就没什么太大优势,因为总是1个核在工作,而另外的核却在休息,效率肯定低下,而此时用多线程,就可以充分使用计算机的处理器资源。同时对于一些有IO限制的操作而言,如读取磁盘文件,网络数据相关操作时,整个过程并不是完全靠运算,而是要通过磁盘驱动器或者网络驱动器来协助完成,比如读取磁盘中的一个文件,当应用程序的读取线程发出读取请求的时候,该请求会被磁盘驱动器所排队处理,假如它是个很长的操作,那么该操作会在磁盘驱动器上排队或者执行很长时间,而这段时间读线程就处于阻塞的状态,这样就浪费了线程资源,正确的做法应该是线程将读请求发送到磁盘驱动器后马上返回,继续处理其他任务,而当磁盘驱动器操作完成的时候,由磁盘驱动器来通知或者由一个线程来轮询执行状态。这样就防止线程资源被浪费,从而提高系统性能。
 
总结一下上面的说法,异步有三个优点: 

1) 在I/O受限等情况下,异步能提高性能,并且能更加充分利用多核CPU的优点。
2) 异步能增强系统健壮性
3) 异步能改善用户体验 

 

同时也有缺点,如下坏处:

1) 滥用异步,会影响性能
2) 增加编程难度 

 

posted @ 2016-05-13 17:19  徐朗  阅读(501)  评论(0编辑  收藏  举报