我目前正在将客户端应用程序迁移到.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";
}
现在,在客户端,此服务是使用渠道工厂创建的。这意味着我只能访问服务契约定义的方法,除非我明确地定义和实现它们,否则我特别无法访问异步服务方法。
根据现有的方法,我有两个选择:
我可以通过打包调用来异步调用同步服务:
await Task.Run<string>(() => svc.GetTest());
我可以直接异步调用异步服务,服务由服务器提供:
await svc.GetTestAsync();
两者都运行正常,不会阻止客户端。这两种方法都涉及在某一端忙于等待:选项1在客户端上等待,这相当于之前在后台线程中所做的事情。选项2通过在那里包装同步方法在服务器上等待。
使同步WCF服务具有异步感知能力的推荐方法是什么?我应该在客户端或服务器上执行打包的位置?或者有没有更好的选择,无需等待任何地方,即通过在连接上引入“真正的”异步性 - 就像生成的代理一样?
答案 0 :(得分: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();
}
}
答案 1 :(得分:7)
如果您的服务器端API可以自然异步(就像您的Task.Delay
示例,而不是Task.Run
包装器),请将其声明为Task
- 基于合同界面。否则,只需保持同步(但不要使用Task.Run
)。不要为同一方法的同步和异步版本创建多个端点。
生成的WSDL对于异步和同步合同API保持不变,我发现自己: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消息的边界。
答案 2 :(得分:2)
这并不可怕:https://stackoverflow.com/a/23148549/177333。您只需使用Task.FromResult()
包装返回值。您必须更改服务端,但它仍然是同步的,并且您没有使用额外的线程。这会改变您的服务器端接口,该接口仍然可以与客户端共享,因此它可以异步等待。否则看起来你必须以某种方式在服务器和客户端上维护单独的合同。