异步使用同步WCF服务

pok*_*oke 16 .net c# wcf async-await

我目前正在将客户端应用程序迁移到.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";
}
Run Code Online (Sandbox Code Playgroud)

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

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

  1. 我可以通过包装调用来异步调用同步服务:

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

    await svc.GetTestAsync();
    
    Run Code Online (Sandbox Code Playgroud)

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

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

Sco*_*ain 7

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

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

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

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";
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

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

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();
    }
}
Run Code Online (Sandbox Code Playgroud)

  • +1,除了一点:*"您应该在服务器上具有同步功能,并且只有服务器上的同步功能.*".我会说,如果该方法可以在服务器上自然地*async(而不是`Task.Run`包装器)完成,那么在服务器上只有*async*方法.生成的WSDL对于同步和异步合同是相同的,我刚刚发现:http://stackoverflow.com/q/22623922/1768303 (3认同)

nos*_*tio 7

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

生成的WSDL对于异步和同步合同API保持不变,我只是发现了自己: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();
}
Run Code Online (Sandbox Code Playgroud)

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

[ServiceContract]
public interface IExampleService
{
    [OperationContract]
    Task<string> GetTestAsync(bool runSynchronously);
}
Run Code Online (Sandbox Code Playgroud)

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

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

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

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