c# - qué - web service asincrono
Asincrónicamente consumen servicio WCF síncrono. (3)
Actualmente estoy en el proceso de migrar una aplicación cliente a .NET 4.5 para hacer uso de async / await. La aplicación es un cliente para un servicio WCF que actualmente ofrece solo servicios síncronos. Me pregunto ahora, ¿cómo debo consumir de forma asíncrona este servicio síncrono ?
Estoy utilizando las fábricas de canales para conectarme al servicio WCF, utilizando un contrato de servicio que se comparte entre el servidor y el cliente. Como tal, no puedo usar la generación automática de VisualStudio o svcutil
para generar proxies de clientes asíncronos.
He leído esta pregunta relacionada que trata sobre si se debe ajustar la llamada sincrónica en el lado del cliente utilizando Task.Run
, o si se debe extender el contrato de servicio con métodos asíncronos. La respuesta sugiere que tener métodos asíncronos "reales" ofrecidos por el servidor es mejor para el rendimiento del cliente, ya que ningún subproceso tendrá que esperar activamente a que finalice la llamada de servicio. Esto tiene mucho sentido para mí, y significaría que las llamadas síncronas deberían ajustarse en el lado del servidor.
Por otro lado, Stephen Toub se desorienta al hacer esto en general en esta publicación de blog . Ahora, él no menciona WCF allí, así que no estoy seguro de si esto solo se aplica a las bibliotecas que se ejecutan en la misma máquina, o si también se aplica a las cosas que se ejecutan de forma remota, pero donde la introducción de asynchronicity tiene un impacto real en el conexión / transferencia.
Y, después de todo, como el servidor no funciona de manera asíncrona (y probablemente no lo haga por otro tiempo), algunos hilos siempre tendrán que esperar: en el cliente o en el servidor. Y eso también se aplica cuando se consumen los servicios de forma síncrona (actualmente, el cliente espera en un subproceso en segundo plano para mantener la interfaz de usuario receptiva).
Ejemplo
Para aclarar el problema, he preparado un ejemplo. El proyecto completo está disponible para descargar aquí .
El servidor ofrece un servicio síncrono GetTest
. Este es el que existe actualmente y donde ocurre el trabajo, de manera sincrónica. Una opción sería envolver esto en un método asíncrono, por ejemplo, utilizando Task.Run
, y ofrecer ese método como un servicio adicional en el contrato (que requiere la expansión de la interfaz del contrato).
// 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";
}
Ahora, en el lado del cliente, este servicio se crea utilizando una fábrica de canales. Eso significa que solo tengo acceso a los métodos definidos por el contrato de servicio y, especialmente, no tengo acceso a los métodos de servicio asíncronos a menos que los defino e implemento explícitamente.
Dependiendo de qué métodos están disponibles ahora, tengo dos opciones:
Puedo llamar asíncronamente al servicio síncrono envolviendo la llamada:
await Task.Run<string>(() => svc.GetTest());
Puedo llamar al servicio asíncrono de forma asíncrona directamente, que proporciona el servidor:
await svc.GetTestAsync();
Ambos funcionan bien, y no bloquearán al cliente. Ambos métodos implican la espera ocupada en algún extremo: la Opción 1 espera en el cliente, que es equivalente a lo que se ha hecho anteriormente en un hilo de fondo. La opción 2 espera en el servidor envolviendo el método síncrono allí.
¿Cuál sería la forma recomendada de hacer que un servicio síncrono de WCF sea asíncrono? ¿Dónde debo realizar el ajuste, en el cliente o en el servidor? ¿O hay mejores opciones para hacer esto sin tener que esperar en ningún lugar, es decir, introduciendo asincronicidad "real" en la conexión, como hacen los proxies generados?
El lado del cliente y el lado del servidor están totalmente separados desde un punto de vista asíncrono, no se preocupan el uno por el otro. Debería tener su función de sincronización en su servidor y solo la función de sincronización en su servidor.
Si desea hacerlo "correctamente", en el cliente no podrá reutilizar la misma interfaz para generar su fábrica de canales como la interfaz que se utiliza para generar el servidor.
Por lo que su servidor se vería así
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";
}
}
}
y tu lado del cliente se vería así
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();
}
}
Esto no es horrible: https://.com/a/23148549/177333 . Simplemente envuelve sus valores de retorno con Task.FromResult()
. Tienes que cambiar el lado del servicio, pero sigue siendo síncrono y no estás usando un hilo adicional. Eso cambia la interfaz del lado del servidor que aún se puede compartir con el cliente para que pueda esperar de forma asíncrona. De lo contrario, parece que tiene que mantener contratos separados en el servidor y el cliente de alguna manera.
Si su API del lado del servidor puede ser naturalmente asíncrona (como su ejemplo Task.Delay
, en lugar del envoltorio Task.Run
), declare que se Task.Run
en la Task
en la interfaz del contrato. De lo contrario, simplemente déjelo sincrónico (pero no use Task.Run
). No cree múltiples puntos finales para las versiones de sincronización y asíncrona del mismo método.
El WSDL generado sigue siendo el mismo para las API de contrato asíncrono y de sincronización, acabo de descubrir que yo mismo: diferentes formas de la interfaz de contrato de servicio WCF . Sus clientes seguirán funcionando sin cambios. Al hacer su método WCF del lado del servidor asíncrono, todo lo que hace es mejorar la escalabilidad del servicio. Lo cual es una gran cosa que hacer, por supuesto, pero envolver un método síncrono con Task.Run
preferiría perjudicar la escalabilidad que mejorarla.
Ahora, el cliente de su servicio WCF no sabe si el método se implementa como síncrono o asíncrono en el servidor, y no necesita saberlo. El cliente puede llamar a su método de forma síncrona (y bloquear el subproceso del cliente) o puede llamarlo de forma asíncrona (sin bloquear el subproceso del cliente). En cualquier caso, no cambiará el hecho de que el mensaje de respuesta SOAP se enviará al cliente solo cuando el método se haya completado completamente en el servidor.
En su proyecto de prueba , está intentando exponer diferentes versiones de la misma API con diferentes nombres de contrato:
[ServiceContract]
public interface IExampleService
{
[OperationContract(Name = "GetTest")]
string GetTest();
[OperationContract(Name = "GetTestAsync")]
Task<string> GetTestAsync();
[OperationContract(Name = "GetTestRealAsync")]
Task<string> GetTestRealAsync();
}
Esto realmente no tiene sentido, a menos que desee darle a su cliente una opción para controlar si el método se ejecuta de forma síncrona o asíncrona en el servidor . No puedo ver por qué querría esto , pero incluso si tiene su razón, sería mejor que lo controlara mediante un argumento de método y una única versión de la API:
[ServiceContract]
public interface IExampleService
{
[OperationContract]
Task<string> GetTestAsync(bool runSynchronously);
}
Luego, en la implementación tu podrías hacer:
Task<string> GetTestAsync(bool runSynchronously)
{
if (runSynchronously)
return GetTest(); // or return GetTestAsyncImpl().Result;
else
return await GetTestAsyncImpl();
}
@usr explica esto con gran detalle here . Para resumir, no es como que el servicio WCF devuelve la llamada a su cliente para notificarle la finalización de la operación asíncrona . Más bien, simplemente envía la respuesta SOAP completa utilizando el protocolo de red subyacente cuando se realiza. Si necesita más que eso, podría usar las devoluciones de llamada de WCF para cualquier notificación de servidor a cliente, pero eso abarcaría los límites de un solo mensaje SOAP.