异步消费同步的WCF服务

17

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

我正在使用通道工厂连接WCF服务,利用了在服务器和客户端之间共享的服务契约。因此,我无法使用Visual Studio或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服务支持异步?我应该在客户端还是服务器上执行包装?或者有更好的选项可以在不必等待任何地方的情况下实现“真正”的异步性连接,例如生成的代理所做的那样?

1
@Servy 客户端 在不同的机器上,通过网络消费服务。当然,让客户端完全异步化是有意义的,更不用说没有阻塞的用户界面了。 - poke
3
如果您在API周围调用同步方法的时候使用Task.Run,那么您可能做错了什么。当客户端向服务器发出网络请求时,异步请求是可以接受的,而服务器同步处理该请求也是可以接受的。在这种情况下,异步是针对网络请求的;而该网络请求并不关心服务器方法是异步还是同步的。 - Servy
1
@Servy 这正是我提出这个问题的原因。服务器同步执行其任务,而作为客户端,我希望能够异步地使用这些服务。但我不知道如何正确地实现它。 - poke
2
@Servy 我没有使用代理,因此无法自动生成这样的异步API... - poke
1
个人而言,我宁愿在100个客户端等待响应的单个线程上,也不愿意在服务器上使用100个线程。所有与数据库/设备等的操作在某个时刻都是同步的。因此,如果您使用异步方法扩展服务合同,这只是将工作从客户端转移到服务器。关于您的问题,我认为您应该能够在Task.Factory.StartNew()上执行await... - HiredMind
显示剩余14条评论
3个回答

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

并且您的客户端看起来会像这样。
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();
    }
}

3
除了一个观点:“您应该在服务器上拥有您的同步函数,只有服务器上的同步函数。”我想说,如果这种方法可以在服务器上自然地异步执行(而不是使用Task.Run封装),则仅在服务器上拥有异步方法。同步和异步协议生成的WSDL相同,我刚发现:https://dev59.com/EWEh5IYBdhLWcg3wRBj5 - noseratio - open to work
1
不幸的是,使用通道工厂的主要原因之一是能够在两侧共享相同的接口。但这真的很有趣;只要有一个启用TAP的接口就足以使通道工厂异步化。如果我找不到更好的解决方案,我也许可以“hackishly”以某种方式自己代理调用。 - poke
@poke:你有没有找到一个好的解决方案?当你控制客户端/服务器时,能够共享接口非常好,但我也同意在客户端上进行异步操作会很不错。我希望它们不是互斥的,虽然我看不出当你想在客户端使用任务签名时如何使用服务的非任务签名。你的客户端代理可以透明地做到这一点,但这违背了整个目的,因为使用代理的代码不知道它是一个任务,不能等待等等。 - Nelson Rothermel
@poke:只是在头脑风暴,但也许你可以使用T4模板自动生成接口的异步版本。只要你的Action和ReplyAction命名空间是可预测的,那就不应该太难。 - Nelson Rothermel
@poke True,反射可以让您绕过我提到的限制:使用代理的代码不会知道它是一个任务,也无法等待。无论如何,感谢您的更新。 - Nelson Rothermel
显示剩余3条评论

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

这样做没有什么意义,除非你想为客户提供控制方法在服务器上同步或异步运行的选项。我不明白为什么你要这样做,但即使你有理由,最好通过方法参数和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消息的边界。


2
“这不像WCF服务回调您的客户端一样”我完全没有考虑到这一点,非常感谢!顺便说一下,在示例项目中我给方法命名不好。我添加了显式名称,因为GetTestGetTestAsync彼此冲突(进一步证明了您所说的)-我应该只将它们命名为Test1、Test2和Test3(想法是展示不同的实现可能性)。 - 无论如何,您知道在使用通道工厂时是否可以使用非阻塞/异步TCP调用,还是只能使用生成的代理访问它们吗? - poke
2
@poke,我在这方面远非专家,但我认为生成的代理在幕后使用通道工厂,您可能需要查看生成的代码。所以这应该是可能的。您可能希望将此作为单独的问题提出。顺便说一句,在WSDL元数据中,只会有string Test()方法,即使您在契约中声明为Task<string> TestAsync()。这再次证实了服务器和客户端上的异步对于给定方法来说是完全独立的事实。 - noseratio - open to work

2

这并不糟糕:https://dev59.com/y37aa4cB1Zd3GeqPvdei#23148549。你只需使用Task.FromResult()来包装返回值即可。你需要更改服务端,但仍然是同步的,而且不需要额外的线程。这将改变您的服务器端接口,客户端仍然可以异步等待。否则,看起来您需要在服务器和客户端上分别维护不同的协议。


1
在我看来,这是一个更好的答案:https://dev59.com/d14b5IYBdhLWcg3w3k2Y#28635558 - Nelson Rothermel

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接