Async WCF self hosted service

2020-05-27 07:47发布

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

enter image description here

Server

enter image description here

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;
    }
}

1条回答
Lonely孤独者°
2楼-- · 2020-05-27 08:38

You need to apply ConcurrencyMode.Multiple.

This is where the terminology gets a bit confusing, because in this case it doesn't actually mean "multi-threaded" as the MSDN docs state. It means concurrent. By default (single concurrency), WCF will delay other requests until the original operation has completed, so you need to specify multiple concurrency to permit overlapping (concurrent) requests. Your SynchronizationContext will still guarantee only a single thread will process all the requests, so it's not actually multi-threading. It's single-threaded concurrency.

On a side note, you might want to consider a different SynchronizationContext that has cleaner shutdown semantics. The SingleThreadSynchronizationContext you are currently using will "clamp shut" if you call Complete; any async methods that are in an await are just never resumed.

I have an AsyncContext type that has better support for clean shutdowns. If you install the Nito.AsyncEx NuGet package, you can use server code like this:

static SynchronizationContext syncCtx;
static ServiceHost serviceHost;

static void Main(string[] args)
{
    AsyncContext.Run(() =>
    {
        syncCtx = SynchronizationContext.Current;
        syncCtx.OperationStarted();
        serviceHost = new ServiceHost(typeof(MessageService));
        Console.CancelKeyPress += Console_CancelKeyPress;

        var binding = new NetNamedPipeBinding(NetNamedPipeSecurityMode.None);
        serviceHost.AddServiceEndpoint(typeof(IMessageService), binding, address);
        serviceHost.Open();
    });
}

static void Console_CancelKeyPress(object sender, ConsoleCancelEventArgs e)
{
    if (serviceHost != null)
    {
        serviceHost.BeginClose(_ => syncCtx.OperationCompleted(), null);
        serviceHost = null;
    }

    if (e.SpecialKey == ConsoleSpecialKey.ControlC)
        e.Cancel = true;
}

This will translate Ctrl-C into a "soft" exit, meaning the application will continue running as long as there are client connections (or until the "close" times out). During the close, existing client connections can make new requests, but new client connections will be rejected.

Ctrl-Break is still a "hard" exit; there's nothing you can do to change that in a Console host.

查看更多
登录 后发表回答