Underscore
Underscore

Reputation: 1057

NetTcpBinding and async/await WCF blocking

We are creating a shared WCF channel to use with an async operation:

var channelFactory = new ChannelFactory<IWcfService>(new NetTcpBinding {TransferMode = TransferMode.Buffered});

channelFactory.Endpoint.Behaviors.Add(new DispatcherSynchronizationBehavior(true, 25));
var channel = channelFactory.CreateChannel(new EndpointAddress(new Uri("net.tcp://localhost:80/Service").AbsoluteUri + "/Test"));

This calls the following service:

[ServiceContract]
public interface IWcfService
{
    [OperationContract]
    Task<MyClass> DoSomethingAsync();
}

[ServiceBehavior(ConcurrencyMode = ConcurrencyMode.Multiple, InstanceContextMode = InstanceContextMode.PerCall)]
public class WcfServiceImpl : IWcfService
{
    public Task<MyClass> DoSomethingAsync()
    {
        Thread.Sleep(4000);

        return Task.FromResult(new MyClass());
    }
}

[Serializable]
public class MyClass
{
    public string SomeString { get; set; }      
    public MyClass Related { get; set; }        
    public int[] Numbers { get; set; }
}

If we start 3 requests at once and simulate a long running task on the response:

        using ((IDisposable)channel)
        {
            var task1 = Task.Run(async () => await DoStuffAsync(channel));
            var task2 = Task.Run(async () => await DoStuffAsync(channel));
            var task3 = Task.Run(async () => await DoStuffAsync(channel));

            Task.WaitAll(task1, task2, task3);
        }
    }

    public static async Task DoStuffAsync(IWcfService channel)
    {
        await channel.DoSomethingAsync();

        Console.WriteLine("Response");

        // Simulate long running CPU bound operation
        Thread.Sleep(5000);

        Console.WriteLine("Wait completed");
    }

Then all 3 requests reach the server concurrently, it then responds to all 3 requests at the same time.

However once the response reaches the client it processes each in turn.

Response
// 5 second delay
Wait completed
// Instant
Response
// 5 second delay
Wait completed
// Instant
Response

The responses resume on different threads but only runs 1 per time.

If we use streaming instead of buffered we get the expected behaviour, the client processes all 3 responses concurrently.

We have tried setting max buffer size, using DispatcherSynchronizationBehaviour, different concurrency modes, toggling sessions, ConfigureAwait false and calling channel.Open() explicitly.

There seems to be no way to get proper concurrent responses on a shared session.

Edit

I have added an image of what I believe to be happening, this only happens in Buffered mode, in streamed mode the main thread does not block.

3 threads spawn concurrently but responses are received in series for buffered mode

Upvotes: 1

Views: 680

Answers (1)

phuldr
phuldr

Reputation: 126

@Underscore

I was trying to solve exact same problem recently. Although, I wasn't able to identify exactly why TransferMode.Buffered is causing what seems to be a global lock on a WCF channel until the thread that was using it gets released, I've found this similar issue deadlock after awaiting. They suggest a workaround which is to add RunContinuationsAsynchronously() to your awaits i.e. await channel.DoSomethingAsync().RunContinuationsAsynchronously() where RunContinuationsAsynchronously():

public static class TaskExtensions
{
    public static Task<T> RunContinuationsAsynchronously<T>(this Task<T> task)
    {
        var tcs = new TaskCompletionSource<T>();

        task.ContinueWith((t, o) =>
        {
            if (t.IsFaulted)
            {
                if (t.Exception != null) tcs.SetException(t.Exception.InnerExceptions);
            }
            else if (t.IsCanceled)
            {
                tcs.SetCanceled();
            }
            else
            {
                tcs.SetResult(t.Result);
            }
        }, TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default);

        return tcs.Task;
    }

    public static Task RunContinuationsAsynchronously(this Task task)
    {
        var tcs = new TaskCompletionSource<object>();

        task.ContinueWith((t, o) =>
        {
            if (t.IsFaulted)
            {
                if (t.Exception != null) tcs.SetException(t.Exception.InnerExceptions);
            }
            else if (t.IsCanceled)
            {
                tcs.SetCanceled();
            }
            else
            {
                tcs.SetResult(null);
            }
        }, TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default);

        return tcs.Task;
    }
}

Which separates WCF continuations. Apparently Task.Yield() works too.

It would be nice to actually understand why this is happening though.

Upvotes: 3

Related Questions