Reputation: 1057
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.
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.
Upvotes: 1
Views: 680
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