65.9K
CodeProject 正在变化。 阅读更多。
Home

WCF 节流

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.91/5 (38投票s)

2009年2月13日

CPOL

9分钟阅读

viewsIcon

164881

downloadIcon

6159

WCF 节流不仅仅是关于节流选项。

引言

WCF 节流对于新手来说可能很奇怪,特别是当不同的设置与各种其他选项以不同的方式交互时,这确实使一切都变得相当混乱。

我通过 Google 搜索了关于 WCF 中节流整个概念的资源。如果你使用 Google, there are some very good pages,可惜 MSDN 绝对 **不是** 其中之一,因为它除了类定义之外几乎没有用处。我决定在一篇文档中概述节流的工作原理,并提供一些示例项目来帮助你理解它。

我假设你已经掌握了基本知识,并且在你的生活中至少创建过一个 WCF 服务。该服务将是一个自托管的 WCF 服务,虽然我将在配置中设置节流,但我还将向你展示如何在代码中进行设置。

理论

配置中有三个设置,以及几个我们将重点关注的 ServiceBehaviour 设置。我没有深入探讨类、属性和类层次结构等的具体细节,而是更容易通过代码来理解它们之间的关系。

1. MaxConcurrentCalls

默认值为 16,16 是默认数量。这是由通道处理的最大消息数。这也受 ServiceBehavior 属性的 ConcurrencyMode 影响。

1.1.1. Single

默认值。服务在一个线程上运行。这确实意味着,无论有多少调用线程,调用都是一次处理一个。

1.1.2. Reentrant

(我从 MSDN 借用了这个描述,因为它几乎说明了一切。) 服务实例是单线程的,并接受重入调用。重入服务在调用另一个服务时接受调用;因此,在你进行外呼之前,你需要负责保持对象状态一致,并且在外呼之后必须确认操作本地数据有效。请注意,服务实例仅在通过 WCF 通道调用另一个服务时才会被解锁。在这种情况下,被调用的服务可以通过回调重新进入第一个服务。如果第一个服务不是重入的,调用序列将导致死锁。

1.1.3. Multiple

多个线程可以同时访问服务,并且你需要自己管理多线程。

2. MaxConcurrentInstances

默认值为 Int32.Max (对于那些不记得的人来说,可以写成 2147483647。我完全记得。完全记得。)。正如你所见,这是一个 **非常** 大的数字,所以我们实际上不会去调整它。它的行为很大程度上取决于 ServiceBehavior 属性中设置的 InstanceContextMode 的使用情况。

2.1.1. PerSession

默认值。当对服务进行调用时,会为客户端创建一个会话,每个会话都有自己的服务实例。

2.1.2. PerCall

每次调用服务都会导致该服务的新实例,然后在调用完成后将其处置。

2.1.3. Single

只有一个服务实例。非常简单。还要记住 ConcurrencyMode 也会影响 InstanceContextMode 选项的行为。

3. MaxConcurrentSessions

如果启用了会话(并且你使用的绑定支持会话),那么每个代理将有效地有一个会话,并且每次数据库调用直到不活动超时都会在一个会话中完成。如果对单个会话进行了多次调用,那么在排队发生之前允许的最大调用数将是……是的,MaxConcurrentCalls

服务和主机

我将不深入探讨此服务的具体细节,它是一个自托管的 WCF 服务,带有一个休眠一秒钟的单个方法,以及一个手工制作的代理。(有关代理的更多信息,请参阅我的另一篇文章 此处。)服务中唯一有点意思的是,你会注意到我们在创建服务时生成一个 Guid,并将该 Guid 返回给 Ping() 方法的调用者。这意味着当我们重用类的一个实例时,我们会得到与之前相同的 Guid,但如果类在每次调用时都被处置和创建,则会返回不同的 GUID。

public class Service1 : IService1
{
    Guid sessionGuid = Guid.NewGuid();

    public Guid Ping()
    {
        Thread.Sleep(1*1000);
        return sessionGuid;
    }
}

配置

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <system.serviceModel>
    <bindings>
      <netTcpBinding>
        <binding name="tcpBinding" portSharingEnabled="True"/>
      </netTcpBinding>
    </bindings>
    <services>
      <service name="WCFThrottling.Service1" 
            behaviorConfiguration="WCFThrottling.Service1Behavior">
        <endpoint address ="net.tcp:///Throttle" 
          binding="netTcpBinding" 
          contract="WCFThrottling.IService1" 
          bindingName="tcpBinding"/>
      </service>
    </services>
    <behaviors>
      <serviceBehaviors>
        <behavior name="WCFThrottling.Service1Behavior">
          <serviceDebug includeExceptionDetailInFaults="true" />
        </behavior>
      </serviceBehaviors>
    </behaviors>
  </system.serviceModel>
</configuration>

客户端和测试代码

我添加了一个客户端项目,允许我们生成多个线程。每个线程将调用 Ping() 方法并返回一个 GUID,然后将其输出到屏幕。

class Program
{
    public static int Threads;
    public static int DoneThreads;
    public static int ErrorThreads;
    
    static void Main(string[] args)
    {
        Threads = 20;
        DoneThreads = 0;
        SpawnThreads(Threads);
        Console.ReadLine();
    }

    public delegate bool ThreadDelegate();
    public static void SpawnThreads(int threads)
    {
        for (int i = 0; i < threads; i++)
        {
            ThreadDelegate del = new ThreadDelegate(CallProxy);
            IAsyncResult result = del.BeginInvoke(delegate(IAsyncResult r) 
                                  { EndCall(del.EndInvoke(r)); }, null);
        }
    }

    public static bool CallProxy()
    {
        try
        {
            Console.WriteLine( new Proxy("ThrottleTCP").Ping());
            return true;
        }
        catch (Exception exc)
        {
            Console.WriteLine(-1);
            //Console.WriteLine(exc.Message);//If you want some debugging
            return false;
        }
    }

    public static void EndCall(bool result)
    {
        if (!result)
        {
            ErrorThreads++;
        }

        DoneThreads++;
        if (DoneThreads == Threads)
        {
            Console.WriteLine(string.Format("The {0} threads completed " + 
                              "of which {1} failed.", DoneThreads, ErrorThreads));
        }
    }
}

是的,我确实意识到 DoneThreads 可能会在不正确的时间被递增,并且 (DoneThreads == Threads) 可能永远不会为 true。这种情况发生的可能性足够小,我并不介意,而且这也不是世界末日。

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <system.serviceModel>
    <bindings>
      <netTcpBinding>
        <binding name="tcpBinding" sendTimeout="00:00:05" 
                        receiveTimeout="00:00:05"/>
      </netTcpBinding>
    </bindings>
    <client>
      <endpoint address="net.tcp:///Throttle" binding="netTcpBinding"
                contract="WCFThrottling.IService1" 
                bindingConfiguration="tcpBinding" 
                name="ThrottleTCP"/>
    </client>
  </system.serviceModel>
</configuration>

目前所有与节流相关的设置都保留为默认值(即,未设置)。

简单明了。注意 **非常** 短的超时时间。我们希望服务尽早超时,而我们只调用 Thread.Sleep() 一秒钟。

测试

PerSession10Sessions.JPG

运行此代码,正如它在不到一秒的时间内生成 20 个线程,为每个线程生成代理,并调用 Ping() 方法。这隐式地为每个代理创建了一个会话,最多为默认的 10 个。前 10 个调用然后成功并返回,而接下来的 10 个调用失败。失败的原因是前 10 个线程分别获得了 10 个可用会话中的一个,接下来的 10 个线程等待会话被释放,但在 5 秒后超时并失败。每个会话只有在显式结束会话或达到 SessionTimeout 值后才会释放。

你还会注意到每个 Guid 都不同,因为每次调用都会创建一个新的服务实例。

解决此问题的最简单方法(假设这是一个问题,你可能出于某种原因希望它按此行为)是增加可能的会话数量。这可以通过在配置中通过将以下行添加到 ServiceBehavior 部分(服务部分,以便清楚)来完成,如下所示

<serviceThrottling maxConcurrentSessions="20"/> 

或者,你也可以在服务主机中进行。我现在提到这种方法,并且在其余的示例中,它将放在配置中。哦,选择一种方法,WCF 不喜欢你同时使用两种方法。

ServiceThrottlingBehavior behaviour = new ServiceThrottlingBehavior();
behaviour.MaxConcurrentSessions = 20;
host.Description.Behaviors.Add(behaviour);

完成此操作后,再次运行它,一切都正常工作。

PerSession20Sessions.JPG

现在,如果你将此行添加到服务中……什么都不会改变。

[ServiceBehavior(InstanceContextMode=InstanceContextMode.PerCall)]

这是因为(我找不到明确说明这一点的内容,但这是唯一有意义的)TCP 通道 **本质上** 是有状态的。你无法将其关闭(尝试将 ServiceContract 属性设置为 SessionMode.NotAllowed,它将无法编译),因此它只是假装你没有尝试从 PerSession 更改为 PerCall。我在弄清楚发生了什么事情时,这个地方 **非常** 烦人了一个小时。

在不支持会话的绑定上(例如 basicHttpBinding),这是默认行为。

现在,在不更改配置的情况下,我们可以更改服务的整个行为。

在此行添加到服务实现上方

[ServiceBehavior(InstanceContextMode=InstanceContextMode.Single, 
                 ConcurrencyMode=ConcurrencyMode.Single)] 

(请记住 ConcurrencyMode.Single 是默认值。)

现在,当你运行项目时,即使每个线程(来自客户端)都接收自己的会话,也只有一个服务实例在处理每个请求(你会注意到它是按顺序处理的),每次都返回相同的 Guid。此外,一些以前成功的线程会失败,而另一些以前失败的线程会成功,因为有更多的会话可用。由于只有一个服务在处理 **所有** 请求,Thread.Sleep() 实际上会锁定整个服务,而不仅仅是特定会话的服务(我希望这句话有意义)。

PerSessionSingleSingle.JPG

现在,我们将 ConcurrencyMode 更改为 ConcurrencyMode.Multiple,允许多个线程访问同一个实例。(这里有明显的线程安全问题,但超出了本文的范围。)

[ServiceBehavior(InstanceContextMode=InstanceContextMode.Single, 
                 ConcurrencyMode=ConcurrencyMode.Multiple)]

我们首先注意到的是,我们 **快得多** 地获得了结果,为多线程点赞。Guids 仍然相同,所以仍然是同一个服务;它只是允许多个线程同时访问它。最多 10 个会话仍然导致最后 10 个线程失败。

PerSessionSingleMultiple.JPG

现在,我们改变游戏的玩法。我们不测试我们可以拥有的会话数量,而是测试每个会话的消息数量(MaxConcurrentCalls)。我们不为每个线程生成新的代理,而是使用相同的代理并在每个线程上调用 Ping() 方法。因此,我们得到一个会话,并在其上进行多次调用。不幸的是,由于服务恢复为单线程模式,并且所有请求都通过同一个会话传入并按顺序处理,这在给定的超时时间内根本行不通。

SingleSessionSingle.JPG

所以,是时候让这个服务再次成为 ConcurrencyMode.Multiple 了。

这导致我们的 Guids 的返回速度 **快得多**(请注意,它们都是相同的,单个服务实例,即使我们的 ContextMode 设置回 ContextMode.PerSession)。

SingleSessionMultiple.JPG

为了真正说明 MaxConcurrentCalls 如何限制事物,真的不那么容易,我能想到的最好的方法是将 MaxConcurrentCalls 限制为一次 2 个。

<serviceThrottling maxConcurrentCalls="2"/>

运行项目,并留意项目运行一段时间后,结果开始成批返回,因为服务一次允许两个线程同时访问它。

关注点

要制作一个能展示 MaxConcurrentCalls 区别的项目确实相当困难,以上版本是我能想到的最好的版本了,我相信有更好的方法可以解决这个问题。

此外,这是本文的第二次尝试。CodeProject 编辑器吃掉了第一个版本,我不得不重新做一遍。我认为这篇文章不如第一次那么好,抱歉 : (

© . All rights reserved.