Performance Impact of Instance Context Mode and Concurrency Mode

Performance of your Windows Communication Foundation (WCF) application depends on many different things. Application design, instancing mode, concurrency mode, transport, binding settings, and throttling settings all have effects on the performance of your application. The InstanceContextMode and ConcurrencyMode settings interact with each other and can have a large impact on WCF application performance. This topic discusses how these two settings can affect WCF service performance. Before discussing how these two settings affect WCF, here is a brief description of what each of these settings does.

Instance Context Mode

InstanceContextMode controls how service instances are allocated in response to client calls. InstanceContextMode can be set to the following values:

  • Single – One service instance is allocated for all client calls.

  • PerCall – One service instance is allocated for each client call.

  • PerSession – One service instance is allocated for each client session.

The default setting for InstanceContextMode is PerSession.

For more information about InstanceContextMode see:

Concurrency Mode

ConcurrencyMode controls how multiple threads are allowed into the service at one time. ConcurrencyMode can be set to one of the following values:

  • Single – One thread can enter the service at one time.

  • Reentrant – One thread can enter the service at one time but callbacks are allowed.

  • Multiple – Multiple threads can enter the service at one time.

The default setting for ConcurrencyMode is Single. This value was chosen so that developers do not have to think about synchronization by default. For more information about ConcurrencyMode see: ConcurrencyMode

Instance Context Mode, Concurrency Mode, and performance

The InstanceContextMode and ConcurrencyMode settings can affect each other, therefore when looking at how they can affect performance you must look at both of the settings together. For example when InstanceContextMode is set to PerCall, the ConcurrencyMode setting is ignored. This is because each client call gets routed to a new service instance, therefore only one thread is running in a service instance at one time. When InstanceContextMode is set to Single only one service instance is created and therefore the ConcurrencyMode can affect the throughput of the application.

There are a number of reasons to set InstanceContextMode to Single, these include:

  1. Creation of a service instance requires a lot of processing. Only allowing one instance of the service to be created can lower the amount of processing required when many clients access the service.

  2. A service instance may create many objects. Setting ConcurrencyMode to single can reduce the garbage collection cost because the objects the service creates do not have to be created and destroyed for each call.

  3. Setting ConcurrencyMode to single allows you to share service instances across multiple clients.

If ConcurrencyMode is set to Single and InstanceContextMode is set to Single only one client call gets through at a time. If you have a large number of clients this can cause a major bottleneck.

If your service does the majority of its work on data that must be shared between threads, and consequently multiple threads must access the data in a serial fashion, you will get little performance benefit from setting ConcurrencyMode to Multiple, and in extreme cases the performance of your service may actually degrade.

The performance characteristics of setting InstanceContextMode to Single and ConcurrencyMode to Reentrantwill be similar to setting ConcurrencyMode to Single. This is because only one client thread is allowed into the service at one time.

When InstanceContextMode is set to PerCall, a new service instance is created for each client call and the instance is destroyed when the client call completes. Because service instances are only available while a call is being made, all resources they may access are freed when the call completes. There is some overhead because a new service instance is allocated for each call. There are no synchronization issues, however because each client call gets its own service instance. When a large number of client calls are made, a large number of service instances are created. It is important to have your service instances only allocate those resources that it requires to function.

If ConcurrencyMode is set to Multiple, multiple client calls can get through, but the developer is responsible for manually synchronizing all access to shared data. This means that only one thread can access the shared data at a time, causing all calls that access the shared data to be serialized. This defeats the purpose of setting ConcurrencyMode to Multiple.

When InstanceContextMode is set to PerCall the ConcurrencyMode setting does not have an impact on throughput. Each client call gets its own service instance so each service instance has only one thread calling into it. When InstanceContextMode is set to PerSession, each session gets its own service instance. For more information about sessions, see Using Sessions. When using PerSession you must use a binding that supports sessions. The following table shows which system-provided bindings support sessions. The default session setting is enclosed in parenthesis.

Binding Session (Default)

BasicHttpBinding

(None)

WSHttpBinding

None, Reliable Session, (Security Session)

WSDualHttpBinding

(Reliable Session), Security Session

WSFederationHttpBinding

(None), Reliable Session, Security Session

NetTcpBinding

(Transport), Reliable Session, Security Session

NetNamedPipeBinding

None, (Transport)

NetMsmqBinding

(None), Transport

NetPeerTcpBinding

(None)

MsmqIntegrationBinding

(None)

BasicHttpContextBinding

(None)

NetTcpContextBinding

(Transport), Reliable Session, Security Session

WSHttpContextBinding

None, Reliable Session, (Security Session)

For more information about which system-defined bindings support sessions see System-Provided Bindings and Configuring System-Provided Bindings. When using PerSession, by default each client proxy gets its own dedicated service instance. All calls from that proxy are processed by the same service instance. A single client may create two different proxies, in which case two service instances are created. It is possible for two proxies to share the same session. For more information, see see the InstanceContextSharing sample.

If InstanceContextMode is set to PerSession and ConcurrencyMode is set to Single, each proxy gets its own service instance. All simultaneous calls from a single proxy to the service instance are serialized because only one thread is allowed into the service instance at one time. Because only one thread is allowed into the service at one time, all other calls made on the same proxy will block until that thread exits. If you have a large number of clients that are all making calls this causes a bottleneck.

If InstanceContextMode is set to PerSession and ConcurrencyMode is set to Multiple, each proxy gets its own service instance and simultaneous calls are allowed into the service instance. Because of this the developer must manually synchronize all access to shared data. Again, this has the effect of serialized access to shared data and may degrade performance in the same way as when ConcurrencyMode is set to Single and multiple calls are made on the same proxy. It is important to note that the service will only handle calls concurrently if a single client executed its requests asynchronously, or if multiple clients shared a session.

For light-weighted service instances, the difference in throughput between using Single and PerCall is not significant (about 8%).

Performance Data and Tests

The machine configuration used to gather the performance data discussed in this document is a single server and four client machines connected over two 1 Gbps Ethernet network interfaces. The server is a quad processor AMD 64 2.2 GHz machine running Windows Server 2008. Each of the client machines are dual processor AMD 2.2GHz x86 machines running the same operating system as the server. The system CPU utilization is maintained at nearly 100%.

Four client machines were used to make calls to the WCF service. Each call made to the service passed 1 to 100 objects to the service operation based on the test being performed. Each object consisted of an order, information about the purchaser and the items being purchased.

The tests focused on the server throughput of the WCF technology. This is defined as the number of operations completed in each second. An operation consists of a client sending a request to the service and receiving a reply from the service. In this test the service performs minimal (negligible) amount of processing to produce the reply. An important point to understand is that business logic dominates the cost of a service in a well-constructed SOA solution. By leaving out business logic processing at the service, only the cost of the messaging infrastructure is measured. Some tests required that the class that implements the service did some additional processing. For these tests some logic was placed in the constructor of the service class to generate a set of prime numbers. Each test was run 3 times and the average of the throughput was calculated.

The following code shows the interface implemented by the WCF service used to generate the performance data.

    [ServiceContract]
    public interface ITestContract
    {
        [OperationContract]
        Order[] GetOrders(int NumOrders);
    }

    [DataContract]
    public class OrderLine
    {
        [DataMember]
        public int ItemID;
        [DataMember]
        public int Quantity;
    }

    [DataContract]
    public class Order
    {
        public Order()
        {
        }

        [DataMember]
        public int CustomerID;
        [DataMember]
        public string ShippingAddress1;
        [DataMember]
        public string ShippingAddress2;
        [DataMember]
        public string ShippingCity;
        [DataMember]
        public string ShippingState;
        [DataMember]
        public string ShippingZip;
        [DataMember]
        public string ShippingCountry;
        [DataMember]
        public string ShipType;
        [DataMember]
        public OrderLine[] orderItems;
        [DataMember]
        public string CreditCardType;
        [DataMember]
        public string CreditCardNumber;
        [DataMember]
        public DateTime CreditCardExpiration;
        [DataMember]
        public string CreditCardName;
    }
}

The following code shows the implementation of the service.

[ServiceBehavior]
    public class LightWeightService : ITestContract
    {
        static Order[] orders;
        int upperLimit = 10000;
        static bool CtorWork = false;

        public LightWeightService()
        {
            if (CtorWork)
                DoSomething();
        }

        void DoSomething()
        {
            // Some work done here
         }
        public static void Initialize(int messageSize)
        {
            orders = new Order[messageSize];
            for (int i = 0; i < messageSize; i++)
            {
                Order order = new Order();
                OrderLine[] lines = new OrderLine[2];
                lines[0] = new OrderLine();
                lines[0].ItemID = 1;
                lines[0].Quantity = 10;
                lines[1] = new OrderLine();
                lines[1].ItemID = 2;
                lines[1].Quantity = 5;
                order.orderItems = lines;
                order.CustomerID = 100;
                order.ShippingAddress1 = "012345678901234567890123456789";
                order.ShippingAddress2 = "012345678901234567890123456789";
                order.ShippingCity = "0123456789";
                order.ShippingState = "0123456789012345";
                order.ShippingZip = "12345-1234";
                order.ShippingCountry = "United States";
                order.ShipType = "UPS";
                order.CreditCardType = "VISA";
                order.CreditCardNumber = "0123456789012345";
                order.CreditCardExpiration = DateTime.UtcNow;
                order.CreditCardName = "01234567890123456789";
                orders[i] = order;
            }
        }

        public Order[] GetOrders(int NumOrders)
        {
            return orders;
        }
    }

The service was self-hosted within a console application. The following code shows how the service can be hosted.

public static void Main(string[] args)
{
    LightWeightService.Initialize(100);
    WSHttpBinding binding = new WSHttpBinding(SecurityMode.None, false);
    ((WSHttpBinding)binding).MaxReceivedMessageSize = 2 * 1024 * 1024;
    ((WSHttpBinding)binding).ReaderQuotas.MaxArrayLength = 2 * 1024 * 1024;

    ServiceHost serviceHost = new ServiceHost(typeof(LightWeightService));

    ServiceBehaviorAttribute sba = serviceHost.Description.Behaviors.Find<ServiceBehaviorAttribute>();
    if (sba == null)
    {
       sba = new ServiceBehaviorAttribute();
    }
    sba.ConcurrencyMode = ConcurrencyMode.Single;
    sba.InstanceContextMode = InstanceContextMode.Single;

    serviceHost.AddServiceEndpoint(typeof(ITestContract), binding, "https://localhost:8000/PerfTest");

    serviceHost.Open();

    Console.WriteLine("Service is running...");
    Console.WriteLine("Press Enter to terminate service");
    Console.ReadLine();
}

The client application was also a console application. The following code shows how the client can be implemented.

static void Main(string[] args)
        {
            WSHttpBinding binding;
            ITestContract proxy;
            ChannelFactory<ITestContract> factory;

            binding = new WSHttpBinding(SecurityMode.None, false);
            factory = new ChannelFactory<ITestContract>(binding, new EndpointAddress("https://localhost:8000/PerfTest"));
            proxy = factory.CreateChannel();

            proxy.GetOrders(100);
        }

The following graph illustrates how the number of objects can affect the throughput of a service when InstanceContextMode is set to Single or PerCall and ConcurrencyMode is set to Single.

The following graph illustrates the effect of the various combinations of settings for InstanceContextMode and ConcurrencyMode on throughput.