异步使用同步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 GetTestAsync() { return Task.Run(() => this.GetTest()); } // ideal asynchronous service; not applicable as work is done synchronously public async Task GetTestRealAsync() { await Task.Delay(2000); return "foo"; } 

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

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

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

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

     await svc.GetTestAsync(); 

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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