题 异步使用同步WCF服务


我目前正在将客户端应用程序迁移到.NET 4.5以使用async / await。该应用程序是WCF服务的客户端,该服务目前仅提供同步服务。我现在在想, 我应该如何异步使用此同步服务

我在用 渠道工厂 使用服务器和客户端共享的服务合同连接到WCF服务。因此,我不能使用VisualStudio或者自动生成 svcutil 生成异步客户端代理。

我读过了 这个相关的问题 这是关于是否在客户端使用包装同步调用 Task.Run,或者是否使用异步方法扩展服务合同。答案表明,服务器提供的“真正的”异步方法对于客户端性能更好,因为没有线程必须主动等待服务调用完成。这对我来说很有意义,这意味着同步调用应该包含在服务器端。

另一方面,斯蒂芬·图布(Stephen Toub)在一般情况下拒绝这样做 这篇博文。现在,他没有在那里提到WCF,所以我不确定这是否只适用于在同一台机器上运行的库,或者它是否也适用于远程运行的东西,但是异步性的引入对其有实际影响。连接/传输。

毕竟,由于服务器实际上并不是异步工作(并且可能不会在另一个时间内工作),因此某些线程将始终必须等待:在客户端或服务器上。这同样适用于同步使用服务(当前,客户端在后台线程上等待以保持UI响应)。

为了使问题更清楚,我准备了一个例子。完整的项目可用 在这里下载

服务器提供同步服务 GetTest。这是当前存在的,并且工作同步发生的地方。一种选择是将其包装在异步方法中,例如使用 Task.Run,并将该方法作为合同中的附加服务提供(要求扩展合同接口)。

// currently available, synchronous service
public string GetTest() {
    Thread.Sleep(2000);
    return "foo";
}

// possible asynchronous wrapper around existing service
public Task<string> GetTestAsync() {
    return Task.Run<string>(() => this.GetTest());
}

// ideal asynchronous service; not applicable as work is done synchronously
public async Task<string> GetTestRealAsync() {
    await Task.Delay(2000);
    return "foo";
}

现在,在客户端,此服务是使用渠道工厂创建的。这意味着我只能访问服务契约定义的方法,除非我明确地定义和实现它们,否则我特别无法访问异步服务方法。

根据现有的方法,我有两种选择:

  1. 我可以异步调用 同步 通过打包电话服务:

    await Task.Run<string>(() => svc.GetTest());
    
  2. 我可以异步调用 异步 直接服务,由服务器提供:

    await svc.GetTestAsync();
    

两者都工作正常,不会阻止客户端。这两种方法都涉及在某一端忙于等待:选项1在客户端上等待,这相当于之前在后台线程中所做的事情。选项2通过在那里包装同步方法在服务器上等待。

什么是使异步感知同步WCF服务的推荐方法?我应该在客户端或服务器上执行打包的位置?或者有没有更好的选择,无需等待任何地方,即通过在连接上引入“真正的”异步性 - 就像生成的代理一样?


16
2018-03-25 15:16


起源


@Servy 客户 是在不同的机器上,通过网络消耗服务。当然,让客户端完全异步是有意义的,更不用说没有阻塞UI了。 - poke
如果你在打电话 Task.Run 围绕API的同步方法,你可能做错了什么。从客户端到服务器的网络请求是异步的,当服务器同步进行处理时,没问题。这样做时,异步是围绕网络请求;并且该网络请求没有 关心 如果服务器方法是异步的或同步的。 - Servy
@StephenCleary虽然我没有使用代理,我使用客户端工厂,所以我不会自动拥有该选项。我将不得不自己添加async-capabilites。 - poke
@Servy这正是我问这个问题的原因。服务器同步执行其内容,并且 - 作为其客户端 - 我希望能够异步使用服务。但我不知道如何正确地做到这一点。 - poke
@Servy虽然我没有使用代理,所以我不能自动生成这样的异步API ... - poke


答案:


客户端和服务器端完全独立于异步立场,他们根本不关心彼此。您应该在服务器上具有同步功能,并且只有服务器上的同步功能。

如果要“正确”执行此操作,则在客户端上,您将无法重复使用相同的接口来生成通道工厂,作为用于生成服务器的接口。

所以你的服务器端看起来像这样

using System.ServiceModel;
using System.Threading;

namespace WcfService
{
    [ServiceContract]
    public interface IService
    {
        [OperationContract]
        string GetTest();
    }

    public class Service1 : IService
    {
        public string GetTest()
        {
            Thread.Sleep(2000);
            return "foo";
        }
    }
}

而你的客户端看起来像这样

using System;
using System.Diagnostics;
using System.ServiceModel;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace SandboxForm
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();

            var button = new Button();
            this.Controls.Add(button);

            button.Click += button_Click;
        }

        private async void button_Click(object sender, EventArgs e)
        {
            var factory = new ChannelFactory<IService>("SandboxForm.IService"); //Configured in app.config
            IService proxy = factory.CreateChannel();

            string result = await proxy.GetTestAsync();

            MessageBox.Show(result);
        }
    }

    [ServiceContract]
    public interface IService
    {
        [OperationContract(Action = "http://tempuri.org/IService/GetTest", ReplyAction = "http://tempuri.org/IService/GetTestResponse")]
        Task<string> GetTestAsync();
    }
}

7
2018-03-25 20:07



+1,除了一点: “您应该在服务器上具有同步功能,并且只在服务器上具有同步功能。“。我会说,如果方法可以做到 自然 服务器上的异步(而不是 Task.Run 包装),只有 异步 服务器上的方法。生成的WSDL对于同步和异步合同是相同的,我刚刚发现: stackoverflow.com/q/22623922/1768303 - Noseratio
不幸的是,使用渠道工厂的一个主要原因是能够在任何一方共享相同的界面。但这真的很有趣;只有一个启用TAP的接口足以使通道工厂异步。如果我找不到更好的解决方案,我或许可以(“hackishly”)使用它来以某种方式代理调用。 - poke
@poke:你有没有为此找到一个好的解决方案?当你控制客户端/服务器时,能够共享接口是非常好的,但我也同意能够在客户端上执行异步会很好。我希望它们不是互斥的,但是当你想要在客户端上签署任务签名时,我看不出你如何使用服务的非任务签名。您的客户端代理可以透明地执行此操作,但这会破坏整个目的,因为您使用代理的代码不会知道它是一项任务而无法等待等等。 - Nelson Rothermel
@poke:只是头脑风暴,但也许你可以使用T4模板自动生成一个异步版本的界面。只要你的Action和ReplyAction命名空间是可预测的,那就不应该太难了。 - Nelson Rothermel
@NelsonRothermel通常在使用WCF时我只是使用 svcutil.exe的 为我生成代理。它使它们成为部分类,所以我可以编写自己的东西来扩展它而不修改自动生成的文件。您可以将其设置为在服务项目中将svctuil作为after build事件运行,并让它更新您的其他项目。 (或使用内置的GUI工具来构建代理,但我发现脚本svctuil比编辑更容易维护 Reference.svcmap GUI版本的文件。) - Scott Chamberlain


如果您的服务器端API可以 自然 异步(就像你的 Task.Delay 例如,而不是 Task.Run 包装器),声明为 Task - 基于合同界面。否则,只需保持同步(但不要使用 Task.Run)。不要为同一方法的同步和异步版本创建多个端点。

对于异步和同步合同API,生成的WSDL保持不变,我发现自己: 不同形式的WCF服务合同接口。您的客户将保持不变。通过使服务器端WCF方法异步,您所做的只是提高服务可伸缩性。当然,这是一件好事,但用同步方法包装 Task.Run 宁愿损害可扩展性而不是改进它。

现在,您的WCF服务的客户端不知道该方法是在服务器上实现为同步还是异步,并且它不需要知道。客户端可以同步调用您的方法(并阻止客户端的线程),也可以异步调用它(不阻塞客户端的线程)。在任何一种情况下,它都不会改变SOAP响应消息将被发送到客户端的事实 只要 当方法在服务器上完全完成时。

你的测试项目,您试图在不同的合同名称下公开相同API的不同版本:

[ServiceContract]
public interface IExampleService
{
    [OperationContract(Name = "GetTest")]
    string GetTest();

    [OperationContract(Name = "GetTestAsync")]
    Task<string> GetTestAsync();

    [OperationContract(Name = "GetTestRealAsync")]
    Task<string> GetTestRealAsync();
}

除非您希望为客户端提供控制方法是同步还是异步运行的选项,否则这没有任何意义 在服务器上我看不见 为什么 你会想要这个,但即使你有理由,你最好通过方法参数和API的单个版本来控制它:

[ServiceContract]
public interface IExampleService
{
    [OperationContract]
    Task<string> GetTestAsync(bool runSynchronously);
}

然后,在实施中你可以这样做:

Task<string> GetTestAsync(bool runSynchronously)
{
    if (runSynchronously)
        return GetTest(); // or return GetTestAsyncImpl().Result;
    else
        return await GetTestAsyncImpl();
}

@usr详细解释了这一点 这里。总结一下, 它是  就像WCF服务回调您的客户端以通知异步操作的完成。相反,它只是在完成后使用底层网络协议发回完整的SOAP响应。如果您需要更多,您可以使用 WCF回调 对于任何服务器到客户端通知,但这将跨越单个SOAP消息的边界。


7
2018-03-25 21:45



“它不像WCF服务回叫你的客户” 我完全没有想到这一点,非常感谢!顺便说一句。我在示例项目中命名方法很差。我添加了显式名称,因为 GetTest 和 GetTestAsync 相互冲突(进一步证明你所说的) - 我应该只是将它们命名为Test1,Test2和Test3(这个想法只是为了展示不同的实施可能性)。 - 无论如何,你知道在使用频道工厂时是否可以使用非阻塞/异步TCP调用,或者我是否只能访问具有生成代理的那些? - poke
@poke,我不是这里的专家,但我认为生成的代理使用场景后面的渠道工厂,你可能想看看生成的代码。所以它应该是可能的。您可能想将此问作为一个单独的问题。顺便说一句,请注意,在WSDL元数据中,只有 string Test() 方法,即使你在合同中声明它 Task<string> TestAsync()。这再次证实了服务器和客户端上给定方法的异步完全独立。 - Noseratio


这并不可怕: https://stackoverflow.com/a/23148549/177333。您只需将返回值换行 Task.FromResult()。您必须更改服务端,但它仍然是同步的,并且您没有使用额外的线程。这会改变您的服务器端接口,该接口仍然可以与客户端共享,因此它可以异步等待。否则看起来你必须以某种方式在服务器和客户端上维护单独的合同。


2
2017-09-25 21:20



在我看来,这是一个更好的答案: stackoverflow.com/a/28635558/177333 - Nelson Rothermel