简体   繁体   English

异步WCF自托管服务

[英]Async WCF self hosted service

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. 我的目标是实现异步自托管WCF服务,该服务将在单个线程中运行所有请求并充分利用新的C#5异步功能。

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. 我的服务器将是一个控制台应用程序,我在其中将设置一个SingleThreadSynchronizationContext ,按规定这里 ,创建和打开的ServiceHost ,然后运行SynchronizationContext ,所以所有的WCF请求都在同一个线程来处理。

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 . 如您所见,我要做的第一件事是设置一个线程SynchronizationContext Following, I create, configure and open a ServiceHost. 接下来,我创建,配置和打开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 . 根据这篇文章 ,因为我在创建之前设置了SynchronizationContext, ServiceHost将捕获它,并且所有客户端请求将在SynchronizationContext发布。 In the sequence, I start the SingleThreadSynchronizationContext in the same thread. 在序列中,我在同一个线程中启动SingleThreadSynchronizationContext

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). 客户端以最小间隔(<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). 我希望服务器能够接收第一个调用并在SingleThreadSynchronizationContext运行它(对一个新的WorkItem排队。当达到await关键字时,将再次捕获SynchronizationContext ,向其发送延续,并且该方法将返回一个Task at at这一点,释放SynchronizationContext来处理第二个请求(至少开始处理它)。

As you can see by the Thread's id in the server log, the requests are being correctly posted in the SynchronizationContext . 正如您在服务器日志中的Thread ID所看到的,请求正在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? 实现WCF自托管异步服务器的正确方法是什么?

I think the problem is with the SingleThreadSynchronizationContext, but I can't see how to implement it in any other manner. 我认为问题在于SingleThreadSynchronizationContext,但我看不出如何以任何其他方式实现它。

I researched the subject, but I could not find more useful information on asynchronous WCF service hosting, especially using the Task based pattern. 我研究了这个主题,但是我找不到有关异步WCF服务托管的更多有用信息,特别是使用基于任务的模式。

ADDITION 加成

Here is my implementation of the SingleThreadedSinchronizationContext . 这是我对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;
    }
}

You need to apply ConcurrencyMode.Multiple . 您需要应用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. 这就是术语有点令人困惑的地方,因为在这种情况下,它实际上并不意味着MSDN文档所说的“多线程”。 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. 默认情况下(单一并发),WCF将延迟其他请求,直到原始操作完成 ,因此您需要指定多个并发以允许重叠(并发)请求。 Your SynchronizationContext will still guarantee only a single thread will process all the requests, so it's not actually multi-threading. 您的SynchronizationContext仍然只保证一个线程将处理所有请求,因此它实际上不是多线程的。 It's single-threaded concurrency. 它是单线程并发。

On a side note, you might want to consider a different SynchronizationContext that has cleaner shutdown semantics. 另外,您可能需要考虑具有更清晰的关闭语义的不同SynchronizationContext The SingleThreadSynchronizationContext you are currently using will "clamp shut" if you call Complete ; 如果您调用Complete ,您当前使用的SingleThreadSynchronizationContext将“钳制”; any async methods that are in an await are just never resumed. await中的任何async方法都永远不会恢复。

I have an AsyncContext type that has better support for clean shutdowns. 我有一个AsyncContext类型 ,它更好地支持干净的关闭。 If you install the Nito.AsyncEx NuGet package, you can use server code like this: 如果您安装Nito.AsyncEx NuGet包,您可以使用如下服务器代码:

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). 这会将Ctrl-C转换为“软”退出,这意味着只要有客户端连接(或直到“关闭”超时),应用程序就会继续运行。 During the close, existing client connections can make new requests, but new client connections will be rejected. 在关闭期间,现有客户端连接可以发出新请求,但新客户端连接将被拒绝。

Ctrl-Break is still a "hard" exit; Ctrl-Break仍然是一个“硬”退出; there's nothing you can do to change that in a Console host. 您无法在控制台主机中更改它。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM