【问题标题】:Async WCF self hosted service异步 WCF 自托管服务
【发布时间】:2013-06-09 10:09:57
【问题描述】:

我的目标是实现一个异步自托管 WCF 服务,它将在单个线程中运行所有请求并充分利用新的 C# 5 异步功能。

我的服务器将是一个控制台应用程序,我将在其中设置一个SingleThreadSynchronizationContext,指定here,创建并打开一个ServiceHost,然后运行SynchronizationContext,因此所有WCF请求都在同一个线程。

问题在于,虽然服务器能够成功处理同一线程中的所有请求,但异步操作正在阻塞执行并被序列化,而不是交错。

我准备了一个简化的示例来重现该问题。

这是我的服务合同(服务器和客户端相同):

[ServiceContract]
public interface IMessageService
{
    [OperationContract]
    Task<bool> Post(String message);
}

服务实现如下(稍微简化了一点,但最终实现可能会访问数据库,甚至以异步方式调用其他服务):

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

服务托管在控制台应用程序中:

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

如您所见,我做的第一件事是设置一个单线程SynchronizationContext。接下来,我创建、配置和打开一个 ServiceHost。根据this article,由于我在创建之前设置了 SynchronizationContext,ServiceHost 将捕获它,所有客户端请求都将发布在SynchronizationContext 中。在序列中,我在同一个线程中启动SingleThreadSynchronizationContext

我创建了一个测试客户端,它将以即发即弃的方式调用服务器。

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

当我执行示例时,我得到以下结果:

客户

服务器

客户端以最小间隔(SingleThreadSynchronizationContext 中运行它(排队一个新的WorkItem。当达到await 关键字时,SynchronizationContext 将再次被捕获,继续发布到它,并且该方法此时将返回一个任务,释放SynchronizationContext 以处理第二个请求(至少开始处理它)。

从服务器日志中的线程 ID 可以看出,请求已正确发布在 SynchronizationContext 中。但是,查看时间戳,我们可以看到第一个请求在第二个请求开始之前完成,这完全违背了拥有异步服务器的目的。

为什么会这样?

实现 WCF 自托管异步服务器的正确方法是什么?

我认为问题出在 SingleThreadSynchronizationContext,但我看不出如何以任何其他方式实现它。

我研究了这个主题,但找不到更多关于异步 WCF 服务托管的有用信息,尤其是使用基于任务的模式。

添加

这是我对SingleThreadedSinchronizationContext 的实现。和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;
    }
}

【问题讨论】:

  • 为什么是阻塞队列?我会使用 ConcurrentQueue。
  • 嗯,这是文章中建议的实现,但我同意它,因为 SingleThreadSynchronizationContext 无论如何都要等待下一个工作项插入。 BlockingCollection.TryTake 阻塞直到可用,此外它还有一种优雅的方式来取消操作,使用 CancellationToken。为什么在这种情况下 ConcurrentQueue 更可取?执行完一个任务后,你会如何等待下一个工作项?

标签: c# wcf asynchronous async-await servicehost


【解决方案1】:

您需要申请ConcurrencyMode.Multiple

这是术语有点令人困惑的地方,因为在这种情况下,它实际上并不意味着 MSDN 文档所述的“多线程”。这意味着并发。默认情况下(单并发),WCF 会延迟其他请求,直到原始操作完成,因此您需要指定多个并发以允许重叠(并发)请求。您的SynchronizationContext 仍将保证只有一个线程将处理所有请求,因此它不是实际上 多线程。它是单线程并发。

顺便说一句,您可能需要考虑一个不同的SynchronizationContext,它具有更清晰的关闭语义。如果您调用Complete,您当前使用的SingleThreadSynchronizationContext 将“关闭”; await 中的任何 async 方法永远不会恢复。

我有一个AsyncContext type,它对干净关机有更好的支持。如果你安装 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;
}

这会将 Ctrl-C 转换为“软”退出,这意味着只要有客户端连接(或直到“关闭”超时),应用程序就会继续运行。在关闭期间,现有的客户端连接可以发出新的请求,但新的客户端连接将被拒绝。

Ctrl-Break 仍然是一个“硬”退出;您无法在控制台主机中更改它。

【讨论】:

  • ConcurrencyMode.Multiple 是关键。一旦我在服务的行为中配置了它,应用程序就会按预期工作。我要看看 Nito.AsyncEx,它看起来很有希望。谢谢!
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-11-07
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多