My objective is to implement an asynchronous self hosted WCF service which will run all requests in a single thread and make full use of the new C# 5 async features.
My server will be a Console app, in which I will setup a SingleThreadSynchronizationContext
, as specified here, create and open a ServiceHost and then run the SynchronizationContext
, so all the WCF requests are handled in the same thread.
The problem is that, though the server was able to successfully handle all requests in the same thread, async operations are blocking the execution and being serialized, instead of being interlaced.
I prepared a simplified sample that reproduces the issue.
Here is my service contract (the same for server and client):
[ServiceContract]
public interface IMessageService
{
[OperationContract]
Task<bool> Post(String message);
}
The service implementation is the following (it is a bit simplified, but the final implementation may access databases or even call other services in asynchronous fashion):
public class MessageService : IMessageService
{
public async Task<bool> Post(string message)
{
Console.WriteLine(string.Format("[Thread {0} start] {1}", Thread.CurrentThread.ManagedThreadId, message));
await Task.Delay(5000);
Console.WriteLine(string.Format("[Thread {0} end] {1}", Thread.CurrentThread.ManagedThreadId, message));
return true;
}
}
The service is hosted in a Console application:
static void Main(string[] args)
{
var syncCtx = new SingleThreadSynchronizationContext();
SynchronizationContext.SetSynchronizationContext(syncCtx);
using (ServiceHost serviceHost = new ServiceHost(typeof(MessageService)))
{
NetNamedPipeBinding binding = new NetNamedPipeBinding(NetNamedPipeSecurityMode.None);
serviceHost.AddServiceEndpoint(typeof(IMessageService), binding, address);
serviceHost.Open();
syncCtx.Run();
serviceHost.Close();
}
}
As you can see, the first thing I do is to setup a single threaded SynchronizationContext
. Following, I create, configure and open a ServiceHost. According to this article, as I've set the SynchronizationContext prior to its creation, the ServiceHost
will capture it and all the client requests will be posted in the SynchronizationContext
. In the sequence, I start the SingleThreadSynchronizationContext
in the same thread.
I created a test client that will call the server in a fire-and-forget fashion.
static void Main(string[] args)
{
EndpointAddress ep = new EndpointAddress(address);
NetNamedPipeBinding binding = new NetNamedPipeBinding(NetNamedPipeSecurityMode.None);
IMessageService channel = ChannelFactory<IMessageService>.CreateChannel(binding, ep);
using (channel as IDisposable)
{
while (true)
{
string message = Console.ReadLine();
channel.Post(message);
}
}
}
When I execute the example, I get the following results:
Client
Server
The messages are sent by the client with a minimal interval ( < 1s).
I expected the server would receive the first call and run it in the SingleThreadSynchronizationContext
(queueing a new WorkItem
. When the await
keyword was reached, the SynchronizationContext
would be once again captured, the continuation posted to it, and the method would return a Task at this point, freeing the SynchronizationContext
to deal with the second request (at least start dealing with it).
As you can see by the Thread's id in the server log, the requests are being correctly posted in the SynchronizationContext
. However, looking at the timestamps, we can see that the first request is being completed before the second is started, what totally defeats the purpose of having a async server.
Why is that happening?
What is the correct way of implementing a WCF self hosted async server?
I think the problem is with the SingleThreadSynchronizationContext, but I can't see how to implement it in any other manner.
I researched the subject, but I could not find more useful information on asynchronous WCF service hosting, especially using the Task based pattern.
ADDITION
Here is my implementation of the SingleThreadedSinchronizationContext
. It is basically the same as the one in the article:
public sealed class SingleThreadSynchronizationContext
: SynchronizationContext
{
private readonly BlockingCollection<WorkItem> queue = new BlockingCollection<WorkItem>();
public override void Post(SendOrPostCallback d, object state)
{
this.queue.Add(new WorkItem(d, state));
}
public void Complete() {
this.queue.CompleteAdding();
}
public void Run(CancellationToken cancellation = default(CancellationToken))
{
WorkItem workItem;
while (this.queue.TryTake(out workItem, Timeout.Infinite, cancellation))
workItem.Action(workItem.State);
}
}
public class WorkItem
{
public SendOrPostCallback Action { get; set; }
public object State { get; set; }
public WorkItem(SendOrPostCallback action, object state)
{
this.Action = action;
this.State = state;
}
}