Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Does ConcurrencyMode of Multiple have relevance when InstanceContextMode is PerCall for a WCF service with Net.Tcp binding?

I always thought that setting InstanceContextMode to PerCall makes concurrency mode irrelevant even if using a session aware binding like net.tcp. This is what MSDN says http://msdn.microsoft.com/en-us/library/ms731193.aspx "In PerCallinstancing, concurrency is not relevant, because each message is processed by a new InstanceContext and, therefore, never more than one thread is active in the InstanceContext."


But today I was going through Juval Lowy's book Programming WCF Services and he writes in Chapter 8

If the per-call service has a transport-level session, whether concurrent processing of calls is allowed is a product of the service concurrency mode. If the service is configured with ConcurrencyMode.Single, concurrent processing of the pending calls is not al lowed, and the calls are dispatched one at a time. [...] I consider this to be a flawed design. If the service is configured with ConcurrencyMode.Multiple, concurrent pro- cessing is allowed. Calls are dispatched as they arrive, each to a new instance, and execute concurrently. An interesting observation here is that in the interest of through- put, it is a good idea to configure a per-call service with ConcurrencyMode.Multiple— the instance itself will still be thread-safe (so you will not incur the synchronization liability), yet you will allow concurrent calls from the same client.


This is contradicting my understanding and what MSDN says. Which is correct ? In my case I have a WCF Net.Tcp service used my many client applications that creates a new proxy object, makes the call and then immediately closes the proxy. The service has PerCall InstanceContextMode. Will I get improved throughput if I change the InstanceContextMode to Multiple with no worse thread safety behaviour than percall ?

like image 231
softveda Avatar asked Sep 01 '12 10:09

softveda


1 Answers

The key phrase in reading Lowy’s statement is “in the interest of throughput”. Lowy is pointing out that when using ConcurrencyMode.Single WCF will blindly implement a lock to enforce serialization to the service instance. Locks are expensive and this one isn’t necessary because PerCall already guarantees that a second thread will never try to call the same service instance.

In terms of behavior: ConcurrencyMode does not matter for a PerCall service instance.

In terms of performance: A PerCall service that is ConcurrencyMode.Multiple should be slightly faster because its not creating and acquiring the (unneeded) thread lock that ConcurrencyMode.Single is using.

I wrote a quick benchmark program to see if I could measure the performance impact of Single vs Multiple for a PerCall service: The benchmark showed no meaningful difference.

I pasted in the code below if you want to try running it yourself.

Test cases I tried:

  • 600 threads calling a service 500 times
  • 200 threads calling a service 1000 times
  • 8 threads calling a service 10000 times
  • 1 thread calling a service 10000 times

I ran this on a 4 CPU VM running Service 2008 R2. All but the 1 thread case was CPU constrained.

Results: All the runs were within about 5% of eachother. Sometimes ConcurrencyMode.Multiple was faster. Sometimes ConcurrencyMode.Single was faster. Maybe a proper statistical analysis could pick a winner. In my opinion they are close enough to not matter.

Here’s a typical output:

Starting Single Service on net.pipe://localhost/base... Type=SingleService ThreadCount=600 ThreadCallCount=500 runtime: 45156759 ticks 12615 msec

Starting Multiple Service on net.pipe://localhost/base... Type=MultipleService ThreadCount=600 ThreadCallCount=500 runtime: 48731273 ticks 13613 msec

Starting Single Service on net.pipe://localhost/base... Type=SingleService ThreadCount=600 ThreadCallCount=500 runtime: 48701509 ticks 13605 msec

Starting Multiple Service on net.pipe://localhost/base... Type=MultipleService ThreadCount=600 ThreadCallCount=500 runtime: 48590336 ticks 13574 msec

Benchmark Code:

Usual caveat: This is benchmark code that takes short cuts that aren’t appropriate for production use.

using System;
using System.Collections.Generic;
using System.Linq;
using System.ServiceModel;
using System.ServiceModel.Description;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace WCFTest
{
    [ServiceContract]
    public interface ISimple
    {
        [OperationContract()]
        void Put();
    }

    [ServiceBehavior(InstanceContextMode = InstanceContextMode.PerCall, ConcurrencyMode = ConcurrencyMode.Single)]
    public class SingleService : ISimple
    {
        public void Put()
        {
            //Console.WriteLine("put got " + i);
            return;
        }
    }

    [ServiceBehavior(InstanceContextMode = InstanceContextMode.PerCall, ConcurrencyMode = ConcurrencyMode.Multiple)]
    public class MultipleService : ISimple
    {
        public void Put()
        {
            //Console.WriteLine("put got " + i);
            return;
        }
    }

    public class ThreadParms
    {
        public int ManagedThreadId { get; set; }
        public ServiceEndpoint ServiceEndpoint { get; set; }
    }

    public class BenchmarkService
    {
        public readonly int ThreadCount;
        public readonly int ThreadCallCount;
        public readonly Type ServiceType; 

        int _completed = 0;
        System.Diagnostics.Stopwatch _stopWatch;
        EventWaitHandle _waitHandle;
        bool _done;

        public BenchmarkService(Type serviceType, int threadCount, int threadCallCount)
        {
            this.ServiceType = serviceType;
            this.ThreadCount = threadCount;
            this.ThreadCallCount = threadCallCount;

            _done = false;
        }

        public void Run(string baseAddress)
        {
            if (_done)
                throw new InvalidOperationException("Can't run twice");

            ServiceHost host = new ServiceHost(ServiceType, new Uri(baseAddress));
            host.Open();

            Console.WriteLine("Starting " + ServiceType.Name + " on " + baseAddress + "...");

            _waitHandle = new EventWaitHandle(false, EventResetMode.ManualReset);
            _completed = 0;
            _stopWatch = System.Diagnostics.Stopwatch.StartNew();

            ServiceEndpoint endpoint = host.Description.Endpoints.Find(typeof(ISimple));

            for (int i = 1; i <= ThreadCount; i++)
            {
                // ServiceEndpoint is NOT thread safe. Make a copy for each thread.
                ServiceEndpoint temp = new ServiceEndpoint(endpoint.Contract, endpoint.Binding, endpoint.Address);
                ThreadPool.QueueUserWorkItem(new WaitCallback(CallServiceManyTimes),
                    new ThreadParms() { ManagedThreadId = i, ServiceEndpoint = temp });
            }

            _waitHandle.WaitOne();
            host.Shutdown();

            _done = true;

            //Console.WriteLine("All DONE.");
            Console.WriteLine("    Type=" + ServiceType.Name + "  ThreadCount=" + ThreadCount + "  ThreadCallCount=" + ThreadCallCount);
            Console.WriteLine("    runtime: " + _stopWatch.ElapsedTicks + " ticks   " + _stopWatch.ElapsedMilliseconds + " msec");
        }

        public void CallServiceManyTimes(object threadParams)
        {
            ThreadParms p = (ThreadParms)threadParams;

            ChannelFactory<ISimple> factory = new ChannelFactory<ISimple>(p.ServiceEndpoint);
            ISimple proxy = factory.CreateChannel();

            for (int i = 1; i < ThreadCallCount; i++)
            {
                proxy.Put();
            }

            ((ICommunicationObject)proxy).Shutdown();
            factory.Shutdown();

            int currentCompleted = Interlocked.Increment(ref _completed);

            if (currentCompleted == ThreadCount)
            {
                _stopWatch.Stop();
                _waitHandle.Set();
            }
        }
    }


    class Program
    {
        static void Main(string[] args)
        {
            BenchmarkService benchmark;
            int threadCount = 600;
            int threadCalls = 500;
            string baseAddress = "net.pipe://localhost/base";

            for (int i = 0; i <= 4; i++)
            {
                benchmark = new BenchmarkService(typeof(SingleService), threadCount, threadCalls);
                benchmark.Run(baseAddress);

                benchmark = new BenchmarkService(typeof(MultipleService), threadCount, threadCalls);
                benchmark.Run(baseAddress);
            }

            baseAddress = "http://localhost/base";

            for (int i = 0; i <= 4; i++)
            {
                benchmark = new BenchmarkService(typeof(SingleService), threadCount, threadCalls);
                benchmark.Run(baseAddress);

                benchmark = new BenchmarkService(typeof(MultipleService), threadCount, threadCalls);
                benchmark.Run(baseAddress);
            }

            Console.WriteLine("Press ENTER to close.");
            Console.ReadLine();

        }
    }

    public static class Extensions
    {
        static public void Shutdown(this ICommunicationObject obj)
        {
            try
            {
                if (obj != null)
                    obj.Close();
            }
            catch (Exception ex)
            {
                Console.WriteLine("Shutdown exception: {0}", ex.Message);
                obj.Abort();
            }
        }
    }
}
like image 85
ErnieL Avatar answered Oct 13 '22 21:10

ErnieL