积极答复者
使用同步上下文的问题

问题
-
以上代码,在点击按钮调用服务,更新文本框,但是每次调用form.sc.Send(callback, null),就会报超时。我觉得已经是在窗口UI的线程中运行了,应该不存在夸线程操作ui控件的问题了。static void Main() { ServiceHost host = new ServiceHost(typeof(Service1)); host.Open(); Application.Run(new Form1()); } public class Service1 : IService1 { [OperationBehavior] public void DoWork() { var form = Application.OpenForms[0] as Form1; SendOrPostCallback callback = _=> { form.Add("Hello world"); }; form.sc.Send(callback, null); } } public partial class Form1 : Form { public SynchronizationContext sc; public Form1() { InitializeComponent(); sc = SynchronizationContext.Current; } private void Button1_Click(object sender, EventArgs e) { var service = ChannelFactory<IService1>.CreateChannel(new WSHttpBinding(),new EndpointAddress("http://localhost:9000/service")); service.DoWork(); } public void Add(string n) { this.textBox1.Text = n; } }
答案
-
你好,
Post用于分发异步消息给同步上下文,Post不会阻塞当前调用的线程。Send用于分发同步消息给上下文,会阻塞当前当前调用的线程,直到调用完成。
https://docs.microsoft.com/en-us/previous-versions/windows/silverlight/dotnet-windows-silverlight/7f1w416x(v%3dvs.95)
https://docs.microsoft.com/en-us/previous-versions/windows/silverlight/dotnet-windows-silverlight/2z9cytsa(v%3dvs.95)
另外UseSynchronizationContext=true是在服务端这边,实现服务同步上下文。让WCF自动检查同步上下文,使得回调能在正确的线程上执行。这个在书上也提到了(参看服务同步上下文),需要调整UI窗口类和服务主机的实例化顺序,就能实现这个功能。功能类似于你在代码中手动实现的,资源同步上下文。使用同步上下文向前台UI线程发送回调消息。它的默认值是true, 即已经默认实现了你手动同步的过程。
https://docs.microsoft.com/zh-cn/dotnet/api/system.servicemodel.servicebehaviorattribute.usesynchronizationcontext?redirectedfrom=MSDN&view=netframework-4.7.2#System_ServiceModel_ServiceBehaviorAttribute_UseSynchronizationContext
1.现在我们考虑你第一个帖子中提到的(使用同步上下文)。假设你在点击事件中没有重开线程。var service = ChannelFactory<IService1>.CreateChannel(new WSHttpBinding(),new EndpointAddress("http://localhost:9000/service")); service.DoWork();
假设我们不在这里重开线程调用的话,正如你在帖子中提到的,这里不存在跨线程调用。这个操作由当前UI线程去调用,在调用的过程中,它等待回调结果。但是回调线程也是当前UI线程,它在返回结果前,想获取之前UI线程的同步上下文,然后返回结果。这等于自己在等待自己,这就产生了死锁。所以出现了之前的错误。
2.现在我们考虑另外一种情况,WCF服务这边仍然使用同步上下文向前台UI线程发送消息(异步同步都可以),但是我们在按钮点击事件中重开一个线程。Thread t1 = new Thread(() => { var service = ChannelFactory<IService1>.CreateChannel(new WSHttpBinding(), new EndpointAddress("http://localhost:9000/service")); service.DoWork(); }); t1.Start();
那么这个时候正是使用同步上下文的场景,它在两个线程之前协调,使得更新UI的操作,在正确的线程上执行。
3.现在我们考虑不使用同步上下文的情况。去掉你手动使用同步上下文发送消息的代码,并显式地指定不使用同步上下文UseSynchronizationContext=false(因为它已经实现了你代码的功能)。[ServiceBehavior(UseSynchronizationContext = false)] public class Service1 : IService1 { public void DoWork() { var form = Application.OpenForms[0] as Form1; //SendOrPostCallback callback = _ => //{ // form.Add("Hello World"); //}; //form.sc.Post(callback, null); form.Add("Hello world"); } }
对单实例的的这个应用而言,这里完全就是UI线程从头到尾执行。它在等待过程中,它也不要获取同步锁,因而结果返回,程序就像在按钮点击事件中直接对文本框赋值一样。
4. 最后我们考虑既使用UseSynchronizationContext属性,也使用你手动实现的代码(使用同步上下文Send或者Post发送回调消息)。
当前台按钮点击事件中重开线程执行,任何情况下均不会死锁(无论UseSynchronizationContext属性值,或者Send/Post方法)。这个很好解释。因为这是使用同步上下文的情况,工作线程和前台UI线程,双线程,并且你的实现正是资源同步上下文封送消息的案例。所以不会死锁。
当前台按钮事件中不重开线程执行调用,当UseSynchronizationContxt=false且仅当使用Post(异步)方法发送消息,不会死锁,其他情况均会死锁。会产生死锁的情况,上面已经解释了,前台线程调用等待自己。这里为什么不会产生死锁,我认为,这可能类似于我们上述的第三种情况,完全不考虑同步锁的问题。因为这是单实例应用程序,不考虑其他复杂情况。
以上,感谢你的测试案例。
Regards
Abraham- 已标记为答案 dna_xp 2019年4月18日 14:49
全部回复
-
我尝试了几个方法,以下是结果
1、使用control.invoke,超时报错
2、当UseSynchronizationContex=false,服务里使用以下代码,运行正常
TaskScheduler scheduler = form.Scheduler; TaskFactory factory = Task.Factory; factory.StartNew(() => { form.sc.Send(callback, null); }, CancellationToken.None, TaskCreationOptions.None, scheduler);
3、以下运行正常
private void Button1_Click(object sender, EventArgs e) { Thread t1 = new Thread(() => { var service = ChannelFactory<IService1>.CreateChannel(new WSHttpBinding(), new EndpointAddress("http://localhost:9000/service")); service.DoWork(); }); t1.Start(); }
- 已编辑 dna_xp 2019年4月17日 13:52
-
我调整了main里servicehost和form生成的顺序,结果还是有区别的。
顺序一,UseSynchronizationContext=true,无法进入服务的方法体,然后超时报错;UseSynchronizationContext=false,可以进入服务方法体,但是调用 form.sc.Send(callback, "sdf");报超时。
var frm = new Form1(); ServiceHost host = new ServiceHost(typeof(Service1)); host.Open(); Application.Run(frm);
顺序二,UseSynchronizationContext不管是true还是false,都可以进入服务方法体,但运行到 form.sc.Send(callback, "sdf");报超时。所以我不知道时什么原因你设置false调用就会没有问题。
ServiceHost host = new ServiceHost(typeof(Service1)); host.Open(); Application.Run(new Form1());
-
你好,
Post用于分发异步消息给同步上下文,Post不会阻塞当前调用的线程。Send用于分发同步消息给上下文,会阻塞当前当前调用的线程,直到调用完成。
https://docs.microsoft.com/en-us/previous-versions/windows/silverlight/dotnet-windows-silverlight/7f1w416x(v%3dvs.95)
https://docs.microsoft.com/en-us/previous-versions/windows/silverlight/dotnet-windows-silverlight/2z9cytsa(v%3dvs.95)
另外UseSynchronizationContext=true是在服务端这边,实现服务同步上下文。让WCF自动检查同步上下文,使得回调能在正确的线程上执行。这个在书上也提到了(参看服务同步上下文),需要调整UI窗口类和服务主机的实例化顺序,就能实现这个功能。功能类似于你在代码中手动实现的,资源同步上下文。使用同步上下文向前台UI线程发送回调消息。它的默认值是true, 即已经默认实现了你手动同步的过程。
https://docs.microsoft.com/zh-cn/dotnet/api/system.servicemodel.servicebehaviorattribute.usesynchronizationcontext?redirectedfrom=MSDN&view=netframework-4.7.2#System_ServiceModel_ServiceBehaviorAttribute_UseSynchronizationContext
1.现在我们考虑你第一个帖子中提到的(使用同步上下文)。假设你在点击事件中没有重开线程。var service = ChannelFactory<IService1>.CreateChannel(new WSHttpBinding(),new EndpointAddress("http://localhost:9000/service")); service.DoWork();
假设我们不在这里重开线程调用的话,正如你在帖子中提到的,这里不存在跨线程调用。这个操作由当前UI线程去调用,在调用的过程中,它等待回调结果。但是回调线程也是当前UI线程,它在返回结果前,想获取之前UI线程的同步上下文,然后返回结果。这等于自己在等待自己,这就产生了死锁。所以出现了之前的错误。
2.现在我们考虑另外一种情况,WCF服务这边仍然使用同步上下文向前台UI线程发送消息(异步同步都可以),但是我们在按钮点击事件中重开一个线程。Thread t1 = new Thread(() => { var service = ChannelFactory<IService1>.CreateChannel(new WSHttpBinding(), new EndpointAddress("http://localhost:9000/service")); service.DoWork(); }); t1.Start();
那么这个时候正是使用同步上下文的场景,它在两个线程之前协调,使得更新UI的操作,在正确的线程上执行。
3.现在我们考虑不使用同步上下文的情况。去掉你手动使用同步上下文发送消息的代码,并显式地指定不使用同步上下文UseSynchronizationContext=false(因为它已经实现了你代码的功能)。[ServiceBehavior(UseSynchronizationContext = false)] public class Service1 : IService1 { public void DoWork() { var form = Application.OpenForms[0] as Form1; //SendOrPostCallback callback = _ => //{ // form.Add("Hello World"); //}; //form.sc.Post(callback, null); form.Add("Hello world"); } }
对单实例的的这个应用而言,这里完全就是UI线程从头到尾执行。它在等待过程中,它也不要获取同步锁,因而结果返回,程序就像在按钮点击事件中直接对文本框赋值一样。
4. 最后我们考虑既使用UseSynchronizationContext属性,也使用你手动实现的代码(使用同步上下文Send或者Post发送回调消息)。
当前台按钮点击事件中重开线程执行,任何情况下均不会死锁(无论UseSynchronizationContext属性值,或者Send/Post方法)。这个很好解释。因为这是使用同步上下文的情况,工作线程和前台UI线程,双线程,并且你的实现正是资源同步上下文封送消息的案例。所以不会死锁。
当前台按钮事件中不重开线程执行调用,当UseSynchronizationContxt=false且仅当使用Post(异步)方法发送消息,不会死锁,其他情况均会死锁。会产生死锁的情况,上面已经解释了,前台线程调用等待自己。这里为什么不会产生死锁,我认为,这可能类似于我们上述的第三种情况,完全不考虑同步锁的问题。因为这是单实例应用程序,不考虑其他复杂情况。
以上,感谢你的测试案例。
Regards
Abraham- 已标记为答案 dna_xp 2019年4月18日 14:49
-
你好,感谢写了那么详细的解释。慢慢拜读,看到哪里如果有问题,还希望不吝赐教。
“想获取之前UI线程的同步上下文,然后返回结果。这等于自己在等待自己,这就产生了死锁。所以出现了之前的错误。”
虽然我认为你说的是正确的原因,但是按照请求应答调用模式:客户端发送请求,阻塞客户端进程,服务端返回操作结果,客户端收到返回结果后继续向下执行。我感觉死锁关键点更可能在阻塞客户端进程这个步骤,由于客户端和服务都是在同一进程中运行,导致当客户端进程被阻塞的情况下,服务方法的运行也被阻塞了,这情况确实表现在我之后的测试当中(方法体并不是在真正修改控件这条语句上锁死,而是方法体都没有进入就已经被锁死,更不要说返回结果的步骤)。
如果我把上下文当成一种资源,那么服务在获得上下文的时候申请同步锁,而客户端没有释放同步锁,导致了死锁情况。不知道我的理解是不是正确,这里有点似是而非的感觉。
- 已编辑 dna_xp 2019年4月18日 16:28