当服务支持回调时,Callback契约一般使用IsOneWay=true, 在控制台的客户端下对于执行回调的服务契约(我们简称服务契约)没有多大限制,但是在UI客户端的情况下,服务契约应该怎么配置呢?我现在给大家来讨论这个问题以及如何解决:
1 [ServiceContract(CallbackContract =typeof(ITalkCallback),SessionMode =SessionMode.Allowed)]//由于支持回调,因此必须使用支持会话的绑定。 2 interface ITalk 3 { 4 [OperationContract(IsOneWay=true)] 5 void Login(TalkMessage m); 6 } 7 8 interface ITalkCallback 9 { 10 [OperationContract(IsOneWay=true)] 11 void BroadCast(TalkMessage m); 12 }
客户端代码
TalkClientProxy client = new TalkClientProxy(new InstanceContext(this)); TalkMessage m = new TalkMessage(); m.Name = txtUserName.Text; m.Message = rtbTalkHistory.Text == string.Empty ? "Login" : rtbTalkHistory.Text; client.Login(m);
服务器端代码
public static Dictionary<string, ITalkCallback> dicCallback = new Dictionary<string, ITalkCallback>(); public virtual void Login(TalkMessage m) { Console.WriteLine(m.Name +"is login"); ITalkCallback callback = OperationContext.Current.GetCallbackChannel<ITalkCallback>(); if (!dicCallback.ContainsKey(m.Name)) { dicCallback.Add(m.Name,callback); } dicCallback.Values.ToList().ForEach(c => c.BroadCast(m)); }
Login的IsOneWay被配置为了true,当客户端在UI线程调用服务的Login函数时有下面这几个步骤:
1.客户端调用Login将直接返回,继续执行UI代码.
2.服务开始调用Login内部代码并执行到回调的BroadCast函数,服务直接返回继续执行.
3.客户端调用broadCast函数。
由于之前客户端调用Login时已经返回,所以BroadCast函数将在客户端被执行,不会出现死锁的情况.
单UI线程下这样的配置没有问题,缺点是消息出现丢失或者执行发生异常大家都不知道。
1 [ServiceContract(CallbackContract =typeof(ITalkCallback),SessionMode =SessionMode.Allowed)]//由于支持回调,因此必须使用支持会话的绑定。 2 interface ITalk 3 { 4 [OperationContract(IsOneWay=false)] 5 void Login(TalkMessage m); 6 } 7 8 interface ITalkCallback 9 { 10 [OperationContract(IsOneWay=true)] 11 void BroadCast(TalkMessage m); 12 }
Login的IsOneWay被配置为了false,当客户端在UI线程调用Login函数时会有以下几个步骤
1.客户端调用Login,等待服务执行完返回,所以客户端被阻塞.
2.服务开始调用Login内部代码,执行回调的BroadCast函数,然后直接返回继续执行.
3.客户端现在收到服务的回调通知要调用BroadCast,但是客户端也在等待Login函数的返回.
4.服务发现Broadcast并没有在客户端被执行完毕,所以服务在执行Login时无法返回.
5.客户端再等待Login返回,服务在等待BroadCast在客户端执行才能返回,因此发生了死锁.
我们来看下IsOneWay=true的MSDN说明:IsOneWay=true指定操作是单向操作,只表示它没有响应消息。 如果无法建立连接、出站消息非常大或该服务无法足够快地读取入站信息,则可能会阻塞.这样就很好的解释了第4点为什么服务在调用Login后不能直接返回,因为服务探测到回调的BroadCast有问题.
解决方法:
1.客户端新启动一个线程来调用Login函数
Task task = new Task( () => { client.Login(m); } ); task.Start();
2.服务在调用Login内部代码,执行BroadCast回调,接着执行后面代码
3.BroadCast在客户端被执行, task处于阻塞状态
4.BroadCast执行完毕,服务返回,task恢复运行.
客户端代码
private void btnMain_Click(object sender, EventArgs e) { TalkClientProxy client = new TalkClientProxy(new InstanceContext(this)); TalkMessage m = new TalkMessage(); m.Name = txtUserName.Text; m.Message = rtbTalkHistory.Text == string.Empty ? "Login" : rtbTalkHistory.Text; Task task = new Task( () => { client.Login(m); //执行服务的Login } ); task.Start(); } public virtual void BroadCast(TalkMessage m) { if (context != null) { context.Post(o => { this.rtbTalkHistory.Text += m.Name + " " + m.Message + "\r\n"; //回调给UI控件赋值 }, new object[0]); } }
看上面的描述,好像没发生死锁,但其实细心就会发现为什么UI线程执行回发生死锁而新起一个线程就不发生死锁呢?在第二情况下BroadCast应该是在task线程里执行才对,应该也要被死锁.
我带着这个疑问查看了线程ID,发现了一些问题
1.当在UI线程执行Login的时候,回调的BroadCast如果执行也是在UI线程里
2.当新启动一个task执行Login的时候,UI线程ID是8, 执行Login的线程ID是9,而执行BroadCast线程的ID是11!
太诡异了, UI线程确实是一个不寻常的线程啊,如果是在UI线程执行服务,服务的回调也会在UI线程执行,而其他线程不是,这就解释第二点为什么不会死锁了.
在来扯一点,如果回调函数BroadCast是调用的UI,那么在第一点中不会出现问题,但是在第二点中会报异常,因为你在非UI线程中对UI资源进行读写,因此好的方法是在Form初始化时就给一个
SynchronizationContext赋当前Form的同步上下文引用:
private SynchronizationContext context; public MainForm() { InitializeComponent(); context = WindowsFormsSynchronizationContext.Current; } public virtual void BroadCast(TalkMessage m) { if (context != null) { context.Post(o => { this.rtbTalkHistory.Text += m.Name + " " + m.Message + "\r\n"; //RichTextBox }, new object[0]); } }
这样就可以在非UI线程访问UI资源了.