代码
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 ]
hash=4d94b922c857fed2f581e43ac3098c0b457b2d56&dt=67.e587f2f9918d5e13c9a835a429ddb84c&v=1.0.1&rtick=14631218489798&open_app_id=0&devtype=web&sign=48117636fa7461f14a2f3ae2cd8b1803&" alt="" width="617" height="567" />
[ 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) 增加编程难度