【问题标题】:Asynchronously consume synchronous WCF service异步消费同步 WCF 服务
【发布时间】:2014-03-25 15:16:54
【问题描述】:

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

我正在使用channel factories 连接到 WCF 服务,利用服务器和客户端之间共享的服务合同。因此,我无法使用 VisualStudio 或 svcutil 的自动生成功能来生成异步客户端代理。

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

另一方面,Stephen Toub 在this blog post 中普遍反对这样做。现在,他在那里没有提到 WCF,所以我不确定这是否仅适用于在同一台机器上运行的库,或者它是否也适用于远程运行的东西,但异步性的引入对连接/传输。

毕竟,由于服务器实际上并没有异步工作(而且很可能不会再有一段时间),一些线程将总是需要等待:无论是在客户端还是在服务器上。这也适用于同步使用服务时(当前,客户端等待后台线程以保持 UI 响应)。

示例

为了让问题更清楚,我准备了一个例子。完整的项目可用于download here

服务器提供同步服务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 服务异步感知的推荐方法是什么?我应该在哪里执行包装,在客户端还是在服务器上?或者有没有更好的选择来做到这一点而无需在任何地方等待,即通过在连接上引入“真正的”异步——就像生成的代理一样?

【问题讨论】:

  • @Servy 客户端在不同的机器上,通过网络使用服务。当然,让客户端完全异步是有意义的,更不用说没有阻塞的 UI。
  • 如果您围绕 API 的同步方法调用 Task.Run,您可能做错了什么。当服务器同步进行处理时,从客户端到服务器的网络请求是异步的,这很好。这样做时,异步围绕网络请求;并且该网络请求不关心服务器方法是异步还是同步。
  • @StephenCleary 我没有使用代理,我使用客户端工厂,所以我不会自动拥有该选项。我必须自己添加异步功能。
  • @Servy 这正是我问这个问题的原因。服务器同步地完成它的工作,并且——作为它的客户端——我希望能够异步地使用这些服务。但我不知道如何正确地做到这一点。
  • @Servy 我没有使用代理,所以我无法自动生成这样的异步 API……

标签: c# .net wcf async-await


【解决方案1】:

从异步的角度来看,客户端和服务器端是完全分开的,它们根本不关心彼此。您的服务器上应该有同步功能,并且只有服务器上的同步功能。

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

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

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();
    }
}

【讨论】:

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

如果您的服务器端 API自然 可以异步(例如您的 Task.Delay 示例,而不是 Task.Run 包装器),请在合约接口中将其声明为基于 Task。否则,让它保持同步(但不要使用Task.Run)。不要为同一方法的同步和异步版本创建多个端点。

对于异步和同步合约 API,生成的 WSDL 保持不变,我自己刚刚发现:Different forms of the WCF service contract interface。您的客户将保持不变。通过使您的服务器端 WCF 方法异步,您所做的就是提高服务的可伸缩性。当然,这是一件很棒的事情,但是用 Task.Run 包装一个同步方法会损害可伸缩性而不是改进它。

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

your test project 中,您试图以不同的合约名称公开同一 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 详细解释了这一点here。综上所述,类似于 WCF 服务回调您的客户端以通知异步操作的完成。相反,它只是在完成后使用底层网络协议发回完整的 SOAP 响应。如果您需要更多,您可以将WCF callbacks 用于任何服务器到客户端的通知,但这会跨越单个 SOAP 消息的边界。

【讨论】:

  • “这不像 WCF 服务回调你的客户端” 我完全没有想到,非常感谢!顺便提一句。我在示例项目中对这些方法的命名很差。我添加了显式名称,因为 GetTestGetTestAsync 相互冲突(进一步证明了你所说的)——我应该只将它们命名为 Test1、Test2 和 Test3(这个想法是为了显示不同的实现可能性) . – 无论如何,您知道在使用通道工厂时是否可以使用非阻塞/异步 TCP 调用,还是只能使用生成的代理访问那些?
  • @poke,我远不是这里的专家,但我认为生成的代理在幕后使用通道工厂,您可能需要查看生成的代码。所以应该是可以的。您可能想将此作为一个单独的问题提出。顺便说一句,请注意,在 WSDL 元数据中只有 string Test() 方法,即使您在合同中将其声明为 Task&lt;string&gt; TestAsync()。这再次证实了一个事实,即给定方法的服务器和客户端的异步是完全独立的。
【解决方案3】:

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

【讨论】:

猜你喜欢
  • 2017-01-11
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2016-02-13
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2014-01-13
相关资源
最近更新 更多