Chamadas assíncronas em WCF: bloqueio de threads de callback

O meu projecto final de curso envolve uma componente de comunicação implementada em WCF que está baseada num binding bidireccional. Num dado ponto, o carregamento de dados por parte de um cliente envolve várias operações assíncronas encadeadas; por exemplo: inicialmente é feito o pedido assíncrono A; quando o pedido A está completo é lançado o pedido B, também assíncrono. Durante a execução surgiu um problema: os pedidos assíncronos eram lançados mas os resultados não eram recolhidos (o callback não era invocado) apesar da operação ser executada no serviço. Tudo indicava um problema no cliente. Depois de algumas cabeçadas na parede chegámos a uma conclusão: o problema era bloquear uma thread de callback! Naquele caso o bloqueio era o ponto sincronização de todos os resultados. Mas então, não posso usar uma thread de callback, que supostamente é uma worker thread (do ThreadPool), para fazer trabalho? Estranho… Mas voltando um pouco atrás, vou tentar replicar aqui o problema num exemplo simples.

Vamos considerar o seguinte serviço WCF, configurado com WsDualHttpBinding

[ServiceContract(Namespace = "WCFTests")]
public interface IHelloWorld
{
    [OperationContract]
    string SayHello(string name);
}

public class MyService : IHelloWorld
{
    public string SayHello(string name)
    {
        return String.Format("Hello {0}!", name);
    }
}
<service name="WCFTests.MyService">
        <host>
          <baseAddresses>
            <add baseAddress="http://localhost:8000"  />
          </baseAddresses>
        </host>
        <endpoint address="HelloWorld" 
                  binding="wsDualHttpBinding" 
                  contract="WCFTests.IHelloWorld"/>
      </service>

Para obter um proxy para o serviço com interface assíncrona podemos utilizar a tool svcutil de linha de comando com o switch /async. Ou, mais fácil, podemos adicionar uma ServiceReference através do Visual Studio, indicando nas opções avançadas que pretendemos interface assíncrona.

 

O WCF, como é natural, segue o Asynchronous Programmming Model (APM) do .NET. Assim, o proxy terá um método BeginSayHello – que aceita o parâmetro name e um AsyncCallback – e um método EndSayHello que permite obter o resultado a partir do IAsyncResult devolvido pelo método anterior.

  IAsyncResult BeginSayHello(string name, AsyncCallback callback, object asyncState);       
  string EndSayHello(IAsyncResult result);

O método EndSayHello é bloqueante até que a resposta ao pedido seja obtida pelo que a melhor forma de utilizar este modelo é passar o callback que será invocado por uma thread do ThreadPool quando a operação terminar. Neste callback podemos depois invocar o método EndSayHello e obter o resultado.

Chegando ao problema que é o objectivo deste post, consideremos a seguinte implementação de um cliente para o serviço HelloWorld:

static void Main(string[] args)
{
    HelloWorldClient proxy = new HelloWorldClient();
    proxy.BeginSayHello(
        ".NET", 
        HelloWorldCallback, 
        proxy);
    Thread.Sleep(3000);
    proxy.BeginSayHello(
        "WCF", 
        HelloWorldBlockingCallback, 
        proxy);
    Thread.Sleep(3000);
    proxy.BeginSayHello(
        "never says hello to this one!", 
        HelloWorldCallback, 
        proxy);
    Console.ReadLine();
}

static void HelloWorldCallback(IAsyncResult iar)
{
    HelloWorldClient proxy = (HelloWorldClient)iar.AsyncState;
    Console.WriteLine(proxy.EndSayHello(iar));
}

static void HelloWorldBlockingCallback(IAsyncResult iar)
{
    HelloWorldClient proxy = (HelloWorldClient)iar.AsyncState;
    Console.WriteLine(proxy.EndSayHello(iar));
    // Bloquear a thread de callback indefinidamente
    Monitor.Enter(proxy);
    Monitor.Wait(proxy);
}

O resultado da execução deste programa nunca mostra a última mensagem na consola devido à segunda invocação do método BeginSayHello, que utiliza um callback que bloqueia a thread de callback. Desta forma, não serão processadas mais mensagens de resposta a pedidos assíncronos anteriores. Note-se que mesmo que o bloqueio seja temporário (realizar algum trabalho de forma síncrona, por exemplo), durante esse tempo não são processadas mensagens de resposta. Não sei o motivo desta situação nem se é um bug ou uma limitação de implementação. Se alguém tiver uma justificação, diga! O mais curioso é que se mudarmos o binding para um binding unidireccional (tipo BasicHttpBinding ou WsHttpBinding) o problema não existe.

Nos bindings que dão problemas a ideia que dá é que uma mensagem não é processada enquanto o callback da anterior não terminar. Talvez exista apenas uma thread a fazer o dispatch que, ao ficar bloqueada, impede o dispatch das restantes mensagens.

Vou tentar descobrir o motivo, mas até agora não tive muita sorte nem encontrei um padrão. Se alguém souber, diga! 🙂

Advertisements

One thought on “Chamadas assíncronas em WCF: bloqueio de threads de callback

  1. O Eng.º Pedro Félix reportou o problema no Connect e parece que nos bindings duplex é utilizada apenas umas thread para receber mensagens e executar callbacks. Está explicado o problema e ao que parece é mesmo um bug da plataforma.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s