从不同线程调用时,WCF Duplex 回调方法永远不会执行

Posted

技术标签:

【中文标题】从不同线程调用时,WCF Duplex 回调方法永远不会执行【英文标题】:WCF Duplex callback method never executes when invoked from a different thread 【发布时间】:2016-02-09 17:01:41 【问题描述】:

好吧,我对这件事束手无策。我有 WCF 双工服务。以下是架构的工作原理:

    客户端打开到端点的连接并提供回调实现 该服务接受该请求并在其他线程上执行一些操作(可能是 1 秒可能是 2 分钟,这就是我不使用异步操作的原因) 处理完成后,调用客户端的回调

问题是当服务调用该回调时,似乎什么也没发生。没有错误,什么都没有。经过进一步调查,我在服务器跟踪中发现了一个异常:

The I/O operation has been aborted because of either a thread exit or an application request

这发生在尝试执行回调之后。

客户端永远不会收到响应,或者关闭。所发生的一切是,由于初始请求是在与主线程不同的线程上发出的,因此它会永远在那里等待该线程完成。

最奇怪的是,如果我尝试在客户端调用的操作中调用回调,而不进入另一个线程,一切正常 - 成功调用回调,这让我相信我已经配置服务正确,但存在线程/死锁问题。

这是我调用服务的方式:

SubmissionServiceClient client = CreateClientInstance();
        client.Open();
        Guid executionId = await client.SubmitAsync(submission);
        submissionCompletionSource.Task.Wait(); //waits for the callback to be called (I omitted the extra wiring code for better readability)
        client.Close();

private SubmissionServiceClient CreateClientInstance()

    NetHttpBinding binding = new NetHttpBinding();
    binding.WebSocketSettings.TransportUsage = WebSocketTransportUsage.Always;
    EndpointAddress endpointAddress = new EndpointAddress("ws://localhost:9080/SubmissionRouter");
    InstanceContext instanceContext = new InstanceContext(this);
    SubmissionServiceClient submissionServiceClient = new SubmissionServiceClient(instanceContext,binding,endpointAddress);
    return submissionServiceClient;

这是回调操作:

public void SubmissionProcessed(SubmissionResultDto result)
        
            submissionCompletionSource.TrySetResult(result);
        

这是客户端调用的服务操作:

public Guid Submit(SubmissionDto submission, ISubmissionCallback callback)
        
            ExecutionDto execution = new ExecutionDto()
            
                Id = Guid.NewGuid(),
                Submission = submission
            ;
            RequestExecution(execution); //Queues the execution of the operation
            submissions.Add(execution.Id, callback);

            return execution.Id;
        

这是服务调用客户端回调的地方(此方法在与发出初始请求的线程不同的线程上执行):

                ISubmissionCallback callback = submissions[submissionResult.ExecutionId];
                    callback.SubmissionProcessed(submissionResult);

服务行为:

[ServiceBehavior(InstanceContextMode = InstanceContextMode.Single, ConcurrencyMode = ConcurrencyMode.Reentrant)]

正如您在提交时看到的那样,服务将回调存储在字典中,并与一个 id 配对,稍后它会使用它来检索回调并调用它。我相信它失败的原因是因为我试图在不同的线程上这样做。

编辑: 我在服务中添加了一个 Ping 操作,它调用另一个线程,该线程挂起 3 秒,然后在客户端调用 Pong 函数。

public void Ping()
        
            var callback = OperationContext.Current.GetCallbackChannel<ISubmissionCallback>();
            Task.Run(() =>
            
                System.Threading.Thread.Sleep(3000);
                callback.Pong();
            );
        

客户: 课堂节目 静态无效主要(字符串 [] 参数) NetHttpBinding 绑定 = new NetHttpBinding(); binding.WebSocketSettings.TransportUsage = WebSocketTransportUsage.Always; EndpointAddress endpointAddress = new EndpointAddress("ws://localhost:9080/SubmissionRouter"); InstanceContext instanceContext = new InstanceContext(new Callback()); SubmissionServiceClient submitServiceClient = new SubmissionServiceClient(instanceContext, binding, endpointAddress);

        submissionServiceClient.Ping();

        Console.Read();
    

    public void SubmissionProcessed(SubmissionResultDto result)
    
        throw new NotImplementedException();
    
    class Callback : ISubmissionServiceCallback
    
        public void Pong()
        
            Console.WriteLine("Pong!");
        

        public void SubmissionProcessed(SubmissionResultDto result)
        

        
    

这实际上是成功的。我设法在客户端收到了我的答案。我现在完全迷失了。

【问题讨论】:

作为一个测试,您能否尝试在您的线程上回调一个非常基本的方法,例如不采用任何参数的方法,例如Ping,看看会发生什么?建立连接后尝试限制通道上的任何其他流量/操作,以便简化重新创建。 您是否维护对服务器端连接的引用? (不仅仅是回调引用) 你用submissionCompletionSource.Task.Wait();阻塞了UI线程吗?另外,当你之前有await 时,你为什么要打电话给Wait,你不是更愿意await submissionCompletionSource.Task 吗? @wal 我更新了帖子。我不确定这是否是你想让我尝试的,但它确实给出了一些有趣的结果。我也不确定你的第二个问题是什么意思。如果您问我是否保留对最初从 (SubmissionServiceClient) 发出请求的通道的引用,那么答案是肯定的,因为在调用回调之前该方法永远不会返回。 submitCompletionSource.Task.Wait(); @ScottChamberlain 是的,该行旨在阻止当前线程,直到我得到响应。之前的等待实际上并不重要,因为提交操作几乎立即返回(它所做的只是将请求排队等待执行并生成一个 Guid)。它很可能是一个同步操作。 Task.Wait 和 await Task 有区别吗,如果有,能给我解释一下吗? 【参考方案1】:

如果您使用 submissionCompletionSource.Task.Wait(); 阻塞 UI 线程,这会导致您的死锁。 WCF 回调默认发生在 UI 线程上,您可以使用 CallbackBehaviorAttribute 更改行为

[CallbackBehaviorAttribute(UseSynchronizationContext=false)]
class Callback : ISubmissionServiceCallback

    public void Pong()
    
        Console.WriteLine("Pong!");
    

    public void SubmissionProcessed(SubmissionResultDto result)
    

    

或者不阻塞 UI 线程。

SubmissionServiceClient client = CreateClientInstance();
client.Open();
Guid executionId = await client.SubmitAsync(submission);
await submissionCompletionSource.Task; //awaits for the callback to be called without blocking the UI.
client.Close();

【讨论】:

我已经尝试过 UseSynchronizationContext=false 但这没有帮助。我会尽快尝试您的第二个建议。 你先生真棒!建议 2 奏效了。现在,如果我能理解为什么。 @Phoenix 发生这种情况是因为您同步等待异步操作,here is a in-depth blog about the issue

以上是关于从不同线程调用时,WCF Duplex 回调方法永远不会执行的主要内容,如果未能解决你的问题,请参考以下文章

响应式 WCF 客户端的双工回调或客户端线程

为啥 WCF 服务能够处理来自不同进程的调用而不是来自线程的调用

WCF Duplex中客户端突然崩溃怎么办

当 WCF 方法抛出异常时,使用空响应调用 jQuery 成功回调

在具有 2 个不同 .SVC 文件的多线程环境中调用 WCF 服务。同时调用两个服务时出错

如何使事件回调进入我的 win 表单线程安全?