Artech

Develop every application as an art using the most suitable technologies!

常用链接

统计

积分与排名

网上邻居

我的博文系列

最新评论

[原创]我的WCF之旅(6):在Winform Application中调用Duplex Service出现TimeoutException的原因和解决方案

几个星期之前写了一篇关于如何通过WCF进行 双向通信的文章([原创]我的WCF之旅(3):在WCF中实现双向通信(Bi-directional Communication) ),在文章中我提供了一个如果在Console Application 调用Duplex WCF Service的Sample。前几天有个网友在上面留言说,在没有做任何改动得情况下,把 作为Client的Console Application 换成Winform Application,运行程序的时候总是出现Timeout的错误。我觉得这是一个很好的问题,通过这个问题,我们可以更加深入地理解WCF的消息交换的机制。

1.问题重现

首先我们来重现这个错误,在这里我只写WinForm的代码,其他的内容请参考我的文章。Client端的Proxy Class(DuplexCalculatorClient)的定义没有任何变化。我们先来定义用于执行回调操作(Callback)的类——CalculatorCallbackHandler.cs。代码很简单,就是通过Message Box的方式显示运算的结果。

using System;
using System.Collections.Generic;
using System.Text;
using System.Windows.Forms;
using Artech.DuplexWCFService.Contract;
using System.ServiceModel;

namespace Artech. WCFService.Client
{
    [ServiceBehavior(ConcurrencyMode 
= ConcurrencyMode.Multiple)]
    
public class CalculatorCallbackHandler : ICalculatorCallback
    
{
        
ICalculatorCallback Members
    }

}

接着我们来设计我们的UI,很简单,无需多说。


代码如下

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Text;
using System.Windows.Forms;
using System.Threading;

namespace Artech. WCFService.Client
{
    
public partial class Form1 : Form
    
{
        
private DuplexCalculatorClient _calculator;
        
private double _op1;
        
private double _op2;
        
public Form1()
        
{
            InitializeComponent();
        }

        
private void Form1_Load(object sender, EventArgs e)
        
{
            
this._calculator = new DuplexCalculatorClient(new System.ServiceModel.InstanceContext(new CalculatorCallbackHandler()));
        }

        
private void Calculate()
        
{
            
this._calculator.Add(this._op1, this._op2);
        }

        
private void buttonCalculate_Click(object sender, EventArgs e)
        
{
            
if (!double.TryParse(this.textBoxOp1.Text.Trim(), out this._op1))
            
{
                MessageBox.Show(
"Please enter a valid number","Error", MessageBoxButtons.OK,  MessageBoxIcon.Error);
                
this.textBoxOp1.Focus();
            }

            
if (!double.TryParse(this.textBoxOp2.Text.Trim(), out this._op2))
            
{
                MessageBox.Show(
"Please enter a valid number","Error", MessageBoxButtons.OK,  MessageBoxIcon.Error);
                
this.textBoxOp1.Focus();
            }

            
try
            
{
                
this.Calculate();
            }

            
catch (Exception ex)
            
{
                MessageBox.Show(ex.Message, 
"Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
            }
 
        }

    }

}

启动Host,然后随启动Client,在两个Textbox中输入数字2和3,Click Calculate按钮,随后整个UI被锁住,无法响应用户操作。一分后,出现下面的错误。


我们从上面的Screen Shot中可以看到这样一个很有意思的现象,运算结果被成功的显示,显示,但是有个Exception被抛出:”This request operation sent to http://localhost:6666/myClient/4f4ebfeb-5c84-45dc-92eb-689d631b337f did not receive a reply within the configured timeout (00:00:57.7300000). The time allotted to this operation may have been a portion of a longer timeout. This may be because the service is still processing the operation or because the service was unable to send a reply message. Please consider increasing the operation timeout (by casting the channel/proxy to IContextChannel and setting the OperationTimeout property) and ensure that the service is able to connect to the client.”。

2.原因分析

在我开始分析为什么会造成上面的情况之前,我要申明一点:由于找不到任何相关的资料,以下的结论是我从试验推导出来,我不能保证我的分析是合理的,因为有些细节我自己都还不能自圆其说,我将在后面提到。我希望有谁对此了解的人能够指出我的问题, 我将不胜感激。

我们先来看看整个调用过程的Message Exchange过程,通过前面相关的介绍,我们知道WCF可以采用三种不同的Message Exchange Pattern(MEP)——One-way,Request/Response,Duplex。其实从本质上讲,One-way,Request/Response是两种基本的MEP, Duplex可以看成是这两种MEP的组合——两个One-way,两个Request/Response或者是一个One-way和一个Request/Response。在定义Service Contract的时候,如果我们没有为某个Operation显式指定为One-way (IsOneWay = true), 那么默认采用Request/Response方式。我们现在的Sample就是由两个Request/Response MEP组成的Duplex MEP。


从上图中我们可以很清楚地看出真个Message Exchange过程,Client调用Duplex Calculator Service,Message先从Client传递到Service,Service执行Add操作,得到运算结果之后,从当前的OperationContext获得Callback对象,发送一个Callback 请求道Client(通过在Client注册的Callback Channel:http://localhost:6666/myClient)。但是,由于Client端调用Calculator Service是在主线程中,我们知道一个UI的程序的主线程一直处于等待的状态,它是不会有机会接收来自Service端的Callback请求的。但是由于Callback Operation是采用Request/Response方式调用的,所以它必须要收到来自Client端Reply来确定操作正常结束。这实际上形成了一个Deadlock,可以想象它用过也不能获得这个Reply,所以在一个设定的时间内(默认为1分钟),它会抛出Timeout 的Exception, Error Message就像下面这个样子。

”This request operation sent to http://localhost:6666/myClient/4f4ebfeb-5c84-45dc-92eb-689d631b337f did not receive a reply within the configured timeout (00:00:57.7300000). The time allotted to this operation may have been a portion of a longer timeout. This may be because the service is still processing the operation or because the service was unable to send a reply message. Please consider increasing the operation timeout (by casting the channel/proxy to IContextChannel and setting the OperationTimeout property) and ensure that the service is able to connect to the client.”。

3.解决方案

方案1:多线程异步调用

既然WinForm的主线程不能接受Service的Callback,那么我们就在另一个线程调用Calculator Service,在这个新的线程接受来自Service的Callback。

于是我们改变Client的代码:

private void buttonCalculate_Click(object sender, EventArgs e)
        
{
            
if (!double.TryParse(this.textBoxOp1.Text.Trim(), out this._op1))
            
{
                MessageBox.Show(
"Please enter a valid number","Error", MessageBoxButtons.OK,  MessageBoxIcon.Error);
                
this.textBoxOp1.Focus();
            }


            
if (!double.TryParse(this.textBoxOp2.Text.Trim(), out this._op2))
            
{
                MessageBox.Show(
"Please enter a valid number","Error", MessageBoxButtons.OK,  MessageBoxIcon.Error);
                
this.textBoxOp1.Focus();
            }

            
try
            
{
                Thread newThread 
= new Thread(new ThreadStart(this.Calculate));
                newThread.Start();        
            }

            
catch (Exception ex)
            
{
                MessageBox.Show(ex.Message, 
"Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
            }
             
        }

通过实验证明,这种方式是可行的。

方案2:采用One-way的方式调用Service 和Callback,既然是因为Exception发生在不同在规定的时间内不能正常地收到对应的Reply,那种我就 允许你不必收到Reply就好了——实际上在本例中,对于Add方法,我们根本就不需要有返回结果,我们完全可以使用One-way的方式调用Operation。在这种情况下,我们只需要改变DuplexCalculator和CalculatorCallback的Service Contract定义就可以了。

using System;
using System.Collections.Generic;
using System.Text;
using System.ServiceModel;

namespace Artech.DuplexWCFService.Contract
{
    [ServiceContract(CallbackContract 
= typeof(ICalculatorCallback))]
    
public interface IDuplexCalculator
    
{
        [OperationContract(IsOneWay 
=true)]
        
void Add(double x, double y);
    }

}

从Message Exchange的角度讲,这种方式实际上是采用下面一种消息交换模式(MEP):

进一步地,由于Callback也没有返回值,我们也可以把Callback操作也标记为One-way.

using System;
using System.Collections.Generic;
using System.Text;
using System.ServiceModel;

namespace Artech.DuplexWCFService.Contract
{
    
//[ServiceContract]
    public interface ICalculatorCallback
    
{
        [OperationContract(IsOneWay 
= true)]
        
void  ShowResult(double x, double y, double result);
    }

}

那么现在的Message Exchange成为下面一种方式:

实现证明这两种方式也是可行的。

4 .疑问

虽然直到现在,所有的现象都说得过去,但是仍然有一个问题不能得到解释:如果是因为Winform的主线程不能正常地接受来自Service的Callback才导致了Timeout Exception,那为什么Callback操作能过正常执行呢?而且通过我的实验证明他基本上是在抛出Exception的同时执行的。(参考第2个截图)

WCF相关内容:
[原创]我的WCF之旅(1):创建一个简单的WCF程序
[原创]我的WCF之旅(2):Endpoint Overview
[原创]我的WCF之旅(3):在WCF中实现双向通信(Bi-directional Communication)
[原创]我的WCF之旅(4):WCF中的序列化(Serialization)- Part I
[原创]我的WCF之旅(4):WCF中的序列化(Serialization)- Part II
[原创]我的WCF之旅(5):Service Contract中的重载(Overloading)
[原创]我的WCF之旅(6):在Winform Application中调用Duplex Service出现TimeoutException的原因和解决方案
[原创]我的WCF之旅(7):面向服务架构(SOA)和面向对象编程(OOP)的结合——如何实现Service Contract的继承
[原创]我的WCF之旅(8):WCF中的Session和Instancing Management
[原创]我的WCF之旅(9):如何在WCF中使用tcpTrace来进行Soap Trace
[原创]我的WCF之旅(10): 如何在WCF进行Exception Handling
[原创]我的WCF之旅(11):再谈WCF的双向通讯-基于Http的双向通讯 V.S. 基于TCP的双向通讯

[原创]我的WCF之旅(12):使用MSMQ进行Reliable Messaging
[原创]我的WCF之旅(13):创建基于MSMQ的Responsive Service

posted on 2007-03-29 00:34 Artech 阅读(7106) 评论(32)  编辑 收藏 网摘 所属分类: J. WCF

评论

#1楼 2007-03-29 07:07 Changhong[未注册用户]

问题2: Doesn't it actually prove your guess? If you make Add method One-way, this._calculator.Add(this._op1, this._op2) will return almost immediately(like an asynchronous call, though they are quite different), And UI thread will be free at the time the calculatorService issues the callback.   回复  引用    

#2楼 2007-03-29 09:18 duhaha[未注册用户]

http://*** 有点小意思   回复  引用    

#3楼 2007-03-29 09:46 搜一哈[未注册用户]

收藏先。   回复  引用    

#4楼[楼主] 2007-03-29 12:47 Artech      

@Changhong
恩,One-way可以看成是特殊的异步调用。很好的答案,谢谢!
  回复  引用  查看    

#5楼 2007-03-29 12:50 Adrian.      

WCF: Duplex Operations and UI Threads
http://www.codeproject.com/useritems/WCF_Duplex_UI_Threads.asp">http://www.codeproject.com/useritems/WCF_Duplex_UI_Threads.asp
  回复  引用  查看    

#6楼[楼主] 2007-03-29 13:20 Artech      

@Adrian.
谢谢,这篇文章基本上帮我清除了我的困惑。

看来是我想多了。其实之所以会引起Timeout Exception,是因为出现了这样的一个Deadlock:

UI线程调用Add方法之后,便一直等待该方法返回,但是Service在执行该方法的时候,由于Callback Client。由于Callback操作所在的线程是Client的UI线程,而这个线程一直处于等待状态。

其实和我的分析基本吻合,这样解决这个Deadlock,问题就迎刃而解。这样问题2就不应该是问题。
  回复  引用  查看    

#7楼[楼主] 2007-03-29 13:22 Artech      

但是我还有个疑问,在第一种情况,为什么在Timeout Exception被抛出的时候,几乎在同一时刻会调用Callback操作呢?   回复  引用  查看    

#8楼 2007-03-29 18:00 火狐[未注册用户]

好棒啊!   回复  引用    

#9楼[楼主] 2007-03-30 00:45 Artech      

谢谢上面朋友的帮助,使我更加清楚地了解了WCF的双向通信。同时基于我新的理解,对原文作了修改。   回复  引用  查看    

#10楼 2007-04-13 14:52 Frank[未注册用户]

@Artech
我想是因为deadlock的双方中有一个出现Timeout并抛出异常,那么另外一个就解除了deadlock并返回结果。本例中是client等待server的通信最后成了deadlock的victim,超时返回异常,这时server对client的callback就成功执行了。
我也是基于以上分析给出的个人理解,请指教。
  回复  引用    

#11楼[楼主] 2007-04-13 15:16 Artech      

@Frank
我觉得应该是像你说的的,UI Thread调用WCF Serivce,有一个时候限制,并且在等待过程中,Callback的请求早已抵达Client side。一旦Timeout,UI Thread释放出来,与此同时Callback操作获得UI Thread并被正常执行。
  回复  引用  查看    

#12楼 2007-04-13 15:32 Frank[未注册用户]

另我查材料后发现,在DuplexCalculatorService类上加属性
[ServiceBehavior(ConcurrencyMode=ConcurrencyMode.Reentrant)]
也能解除死锁。
  回复  引用    

#13楼 2007-04-13 15:38 Frank Huang      

sorry,my mistake,这个死锁和以上讨论的死锁属2个概念,forget it.   回复  引用  查看    

#14楼[楼主] 2007-04-13 15:39 Artech      

@Frank
ConcurrencyMode.Reentrant和ConcurrencyMode.Multiple接除死锁和这个例子是两个不同的概念。他们是针对Service本身,而本例是Client端的死锁。
  回复  引用  查看    

#15楼[楼主] 2007-04-13 15:41 Artech      

@Frank Huang
刚回了你的第一个回复,每想到你的第二个回复就来了。呵呵,忽略上个一个回复:)
  回复  引用  查看    

#16楼 2007-08-02 17:04 181314[未注册用户]

@Artech
好像可以这样理解,从线程的角度oneway是异步的,request/response是同步单向,Duplex 是同步双向。
  回复  引用    

#17楼 2007-08-02 17:06 181314[未注册用户]

@Artech
我英文不好,非常感谢楼主的对WCF的分享,
  回复  引用    

#18楼[楼主] 2007-08-06 14:36 Artech      

@181314
:)
  回复  引用  查看    

#19楼 2007-09-19 12:36 大剑师      

我认为:产生死锁的原因在于使用了MessageBox显示,CLient调用后由于是非异步的所以一直在等待返回,但是在CallBack中收到结果后却使用了Response的Messagebox不返回,因而造成死锁   回复  引用  查看    

#20楼 2007-10-31 15:22 neal1985[未注册用户]

博主你好,我已经看了你的WCF之旅6篇了,现在我想用WCF开发一个实时交流系统,请问博主我要怎么应用WCF   回复  引用    

#21楼 2008-02-27 11:17 千年之夏[未注册用户]

看到第六篇了,这个系列很好很强大   回复  引用    

#22楼 2008-02-28 15:08 未注册用户[未注册用户]

Form1实现回调接口ICalculatorCallback也能解决Timeout问题。出现这个错误的原因是调用Service的线程和调用ICalculatorCallback线程不是同一个线程   回复  引用    

#23楼 2008-04-21 22:15 BAsil      

@大剑师
关于Messagebox的造成死锁的原因是不正确的,我把Messagebox.show换成Console.write还是不行
不过我还是不明白为什么UI thread会一直比较忙?它在忙什么啊?
  回复  引用  查看    

#24楼 2008-05-22 10:29 Eric Fanny Lospy[未注册用户]

you should use class "sendorpostcallback"

sendorpostcallback.post(callback fun,null) to call the callback fun of client, then the client will invoke the callback correctly
  回复  引用    

#25楼 2008-06-16 16:22 Aris[未注册用户]

原因是:
在Windows窗体客户端建立的回调对象与UI的SynchronizationContext之间的关联关系,因此到达客户端的回调需要在UI线程上执行,而线程又为等待服务的返回而被阻塞.所以会发生死锁.
解决方法:
就是关闭回调对象与UI的SynchronizationContext之间的关联关系.(摘自WCF服务编程)
做法:
[CallbackBehavior(UserSynchronizationContext = false)]
public class CalculatorCallbackHandler : ICalculatorCallback
{}

  回复  引用    

#26楼[楼主] 2008-08-21 16:36 Artech      

@Aris
http://www.cnblogs.com/artech/archive/2008/08/21/1273021.html" target="_new">http://www.cnblogs.com/artech/archive/2008/08/21/1273021.html
  回复  引用  查看    

#27楼 2008-10-30 11:16 guidguid[未注册用户]

原因如下:
By default WCF will not let a service callback within a service operation to its clients. The reason is that by default the service uses single-threaded access – only one client is allowed to call it at a time. If the service were to allow to call back to its client it could result with a deadlock if the client will call to the service as a result of the callback to the client. To allow callbacks, you need to configure the service for reentrancy – that is, release the lock before the callback, as show in the demo.
参见:
http://www.idesign.net/idesign/DesktopDefault.aspx?tabindex=5&tabid=11
  回复  引用    

#28楼 2008-10-30 11:22 guidguid[未注册用户]

用了工作线程就可以了的原因:
By default callbacks enter the client on worker threads. If the client is a Windows Forms object, you must marshal the calls to the UI thread. By default this is exactly what WCF will do if the client is a Windows Forms form. But if the client uses a worker thread to dispatch the calls, there will be no marshaling. The client may do so to avoid a deadlock if the service tries to call back during a service call. The download shows such a client that uses its own synchronization context to post messages back to its own UI thread to process the callbacks and avoid a deadlock.
  回复  引用    




发表评论

昵称: [登录] [注册]

主页:

邮箱:(仅博主可见)

评论内容:

  登录  注册

[使用Ctrl+Enter键快速提交评论]

0 692032




相关文章:

相关链接: