none
使用同步上下文的问题 RRS feed

  • 问题

  • 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; } }

    以上代码,在点击按钮调用服务,更新文本框,但是每次调用form.sc.Send(callback, null),就会报超时。我觉得已经是在窗口UI的线程中运行了,应该不存在夸线程操作ui控件的问题了。
    2019年4月16日 15:17

答案

  • 你好,

    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
    2019年4月18日 7:52

全部回复

  • 以下是报错信息:

    TimeoutException: 对“http://localhost:9000/service”的 HTTP 请求已超过为 00:01:00 分配的超时。为此操作分配的时间可能是较长超时的一部分

    我如果不运行这条,调用服务就很正常

    2019年4月16日 15:32
  • 你好
    是的,在我这边的表现也是一样的。但是偶尔会调用成功(使用管理员身份运行),不会报错。另外,当我显式地表明不使用同步上下文的时候,调用没有任何问题。
    [ServiceBehavior(UseSynchronizationContext =false)]
    所以主要问题可能是在什么场景下使用同步上下文。如果你有任何进展。欢迎继续讨论。
    Regards
    Abraham
    2019年4月17日 10:30
  • 我尝试了几个方法,以下是结果

    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
    2019年4月17日 13:37
  • 我调整了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());

    2019年4月17日 13:47
  • 我发现如果UseSynchronizationContext =false,使用post调用就没有问题,send不行
    2019年4月17日 14:05
  • 你好,

    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
    2019年4月18日 7:52
  • 你好,感谢写了那么详细的解释。慢慢拜读,看到哪里如果有问题,还希望不吝赐教。

    想获取之前UI线程的同步上下文,然后返回结果。这等于自己在等待自己,这就产生了死锁。所以出现了之前的错误。

    虽然我认为你说的是正确的原因,但是按照请求应答调用模式:客户端发送请求,阻塞客户端进程,服务端返回操作结果,客户端收到返回结果后继续向下执行。我感觉死锁关键点更可能在阻塞客户端进程这个步骤,由于客户端和服务都是在同一进程中运行,导致当客户端进程被阻塞的情况下,服务方法的运行也被阻塞了,这情况确实表现在我之后的测试当中(方法体并不是在真正修改控件这条语句上锁死,而是方法体都没有进入就已经被锁死,更不要说返回结果的步骤)。

    如果我把上下文当成一种资源,那么服务在获得上下文的时候申请同步锁,而客户端没有释放同步锁,导致了死锁情况。不知道我的理解是不是正确,这里有点似是而非的感觉。




    • 已编辑 dna_xp 2019年4月18日 16:28
    2019年4月18日 14:53