您的位置:首页 > WCF >

WCF:并发处理

2013-10-23 14:49 来源:未知编辑:admin点击:

当多个线程同时访问相同的资源的时候就会产生并发,WCF缺省情况下会保护并发访问。
对并发访问需要恰当处理,控制不好不仅会大大降低WCF服务的吞吐量和性能,而且还有可能会导致WCF服务的死锁。
一、WCF并发模型:
在WCF中使用 ServiceBehaviorAttribute中的ConcurrencyMode属性来控制这个设置。ConcurrencyMode属性是个枚举类型,有三个值:ConcurrencyMode.SingleConcurrencyMode.ReentrantConcurrencyMode.Multiple
public enum ConcurrencyMode
{
   Single,//默认,单线程模型
   Reentrant,//单线程,可重入模型,通常用在CallBack调用模型中
   Multiple//多线程模型
}

[AttributeUsage(AttributeTargets.Class)]
public sealed class ServiceBehaviorAttribute : ...
{
   public ConcurrencyMode ConcurrencyMode
   {get;set;}
   //More members
}

1.ConcurrencyMode.Single
单线程处理模式,同一个服务实例不会同时处理多个请求。当服务在处理请求时会对当前服务加锁,如果再有其它请求需要该服务处理的时候,需要排队等候。当服务处理完请求后会自动解锁,队列中的下个请求获取服务资源,继续处理。

例如:我们去银行去办理业务,如果营业厅中只有一个窗口对外服务的话,那当前窗口每次只能处理一个用户请求,如果再有其它用户需要办理业务的话,只能排队等待。直到我办理完业务后,营业窗口才能为队列中下个用户提供服务。

2.ConcurrencyMode.Reentrant
可重入的单线程处理模式,它仍然是单线程处理。服务端一次仍然只能处理一个请求,如果有多个请求同时到达仍然需要排队。与单线程不同的是,请求在处理过程中可以去调用其它服务,等到其它服务处理完成后,再回到原服务等待队列尾排队。在调用其它服务的过程中,会暂时释放锁,其它等待线程会趁机进行服务的调用。这种模式常见于服务端回调客户端的场境中。
例如:我们去银行办理业务,营业厅中还是只有一个窗口对外服务,一次只能处理一个用户请求。我向银行服务员请求办理开户业务,从此刻开始该服务被我锁定,其它人员只能排队等待。在服务员办理业务的过程中,会让我填写开户申请表,这个签字的过程就是服务端对客户端的回调过程。由于填写开户申请表的时间会很长,为了不耽搁后面排队顾客的时间,我暂时释放对服务员的锁定,到旁边填写申请表,让后面的人员办理业务。在我签完字后再回到队列中等待服务,当再轮到我的时候我再次锁定服务,把凭据交给服务员,让他继续处理我的业务,这相当于服务的“重入”过程。 等到业务办完后,我会离开银行柜台,释放对该服务的锁定,等待队列中的下个人员又开始锁定服务办理业务了。

3.ConcurrencyMode.Multiple
多线程模式,多线程模式可以很好地增加系统的吞吐量。当多个用户请求服务实例时,服务并不会加锁,而是同时为多个请求服务。这样一来对所有用户共享的资源就会产生影响,所以这种多线程的访问模式需要对共享资源做好保护,大部份的情况下需我们的手动编写代码来实现多线程之间的访问保护。
例如:我们去吃烤肉串,烤肉串的师傅可以同时为多个顾客烤制,而不用一个一个地排队等待,这就是典型的多线程处理模式。但这种模式如果不对肉串很好保护得的话那可麻烦了,比仿,我要的是3串麻辣味的,你要的是3串香辣味的,他要的是3串原味的,烤肉的时傅在烤制的时候需要对这三份肉串做好保护,防止做出一些“五味俱全”的肉串来。

二、WCF中的并发模型与实例模型的关系。
实例模型与并发模型的关系很密切,对于不同的实例模型,并发所需要的并发管理也不一样。

1.ServiceBehavior(ConcurrencyMode = ConcurrencyMode.Single,InstanceContextMode = InstanceContextMode.PerCall) --Single并发与PerCall实例模型

WCF足迹6:并发1 - Tony - Go ahead!

《图1》
对于PerCall的实例模型,每个客户端请求都会与服务端的一个独立的服务实例进行交互,就不会出现多个客户端请求争用一个服务实例的情况,也就不会出现并发冲突。也就是说这时候ConcurrencyMode = ConcurrencyMode.Single写与不写都没关系,不会影响吞吐量的问题。
Single并发是系统默认的,而PerCall实例模型也是系统默认的。也就是说在默认情况下,WCF服务不会出现并发访问冲突的情况,当然对于静态数据或全局缓存数据会产生并发冲突,后面会谈到如何解决这个问题。
当然在进行回调操作的时候,有可能会出现等待超时的问题。这在下面4会谈到。

服务端代码:
    [ServiceContract]
    public interface ISinglePerCall
    {
        [OperationContract]
        int GetValue1();
        [OperationContract]
        int GetValue2();
    }
    [ServiceBehavior(InstanceContextMode=InstanceContextMode.PerCall, ConcurrencyMode=ConcurrencyMode.Single)]
    public class SinglePerCall : ISinglePerCall
    {
        private int InstanceVariable = 0;
        private static int StaticVariable = 0;
        public int GetValue1()
        {
            return ++InstanceVariable;
        }

        public int GetValue2()
        {
            return ++StaticVariable;
        }
    }

    这里我们定了服务契约ISinglePerCall,它里面包含了两个方法GetValue1()和GetValue2()。GetValue1()的主要作用是把实例变量的值加1返回;GetValue2()的主要作用是把静态变量的值加1返回。
    服务的行为模式是:Single并发+PerCall实例
    由于PerCall实例模式会为每个请求生成一个新的服务实例,所以,我们调用GetValue1()返回的结果应当永远都是1。因为每个服务实例在生成的时候都默认为InstanceVariable设为0;但调用GetValue2()的时候并不会都是1,因为GetValue2()读取的是静态变量自增后的值,静态变量被所有实例所共享,所以它会从1开始递增。由于在这里多个服务实例同时运行,对共享的静态变量的访问有可以产生冲突,故会出现下面的运行结果。要解决这个问题还需要我们手动编写同步代码。
    
客户端代码:
    class SinglePerCall
    {
        private static SRSinglePerCall.SinglePerCallClient client = new Client.SRSinglePerCall.SinglePerCallClient();
        public static void Main(string[] args)
        {
               
            for (int i = 0; i < 10; i++)
            {
                Thread thread = new Thread(new ThreadStart(DoWork));
                thread.Start();
                while (!thread.IsAlive) ;
            }
        }
        public static void DoWork()
        {
            
            Console.WriteLine("线程" + Thread.CurrentThread.GetHashCode() + ":/t实例变量的值:" +client.GetValue1());
            Console.WriteLine("线程" + Thread.CurrentThread.GetHashCode() + ":/t静态变量的值:" +client.GetValue2());
        }
    }

    在客户端中我启用了10个线程向服务端发起调用,并显示线程号、服务端实例变量的结果和静态变量的结果。
        
运行结果:

WCF足迹6:并发1 - Tony - Go ahead!

《图1-1》
从图中我们看到实例变量的值都是1,静态变量的值是出现递增的情形,但由于没有采取同步策略,有可能会出现“线程9”和“线程11”这样的并发冲突的情况。


2.ServiceBehavior(ConcurrencyMode = ConcurrencyMode.Single,InstanceContextMode = InstanceContextMode.PerSession) --Single并发与PerSession实例模型

WCF足迹6:并发1 - Tony - Go ahead!

《图2》
对于PerSession实例模型,每个客户端对应一个服务实例,这样在不同客户端之间不会出现服务实例并发调用的情况,但每个客户端可以采用多线程调用同一个服务实例。由于我们采用了ConcurrencyMode.PerSession并发模式,这使得多线程在调用同一服务实例的时候会进行线程排队,服务每次只对一个线程进行处理。
Single并发模式对多线程的客户端的吞吐量会带来影响,而对多个单线程的客户端访问并不起作用。
服务端代码:
    [ServiceContract(SessionMode=SessionMode.Required)]
    public interface ISinglePerSessison
    {
        [OperationContract]
        int GetValue1();
        [OperationContract]
        int GetValue2();
    }
    [ServiceBehavior(InstanceContextMode = InstanceContextMode.PerSession, ConcurrencyMode = ConcurrencyMode.Single)]
    public class SinglePerSessison : ISinglePerSessison
    {
        private int InstanceVariable = 0;
        private static int StaticVariable = 0;
        public int GetValue1()
        {
            return ++InstanceVariable;
        }

        public int GetValue2()
        {
            return ++StaticVariable;
        }
    }

    由于使用的PerSession实例模式,所以每个客户端共享同一个服务实例,当同一客户端使用多线程来访问服务的话,会访问同一个InstanceVariable变量。由于是Single并发模式,所以不会对InstanceVariable变量产生并发冲突。
客户端代码:
    class SinglePerSession
    {
        private static SRSinglePerSession.SinglePerSessisonClient client = new Client.SRSinglePerSession.SinglePerSessisonClient();
        public static void Main(string[] args)
        {

           for (int i = 0; i < 10; i++)
            {
                Thread thread = new Thread(new ThreadStart(DoWork));
                thread.Start();

                while (!thread.IsAlive) ;
            }
        }
        public static void DoWork()
        {
            Console.WriteLine("线程" + Thread.CurrentThread.GetHashCode() + ":/t实例变量的值:" +client.GetValue1());
            Console.WriteLine("线程" + Thread.CurrentThread.GetHashCode() + ":/t静态变量的值:" +client.GetValue2());
        }
    }

运行结果:

WCF足迹6:并发1 - Tony - Go ahead!

《图2-1》
从图中可以看出,静态变量依然是被不同客户端不同的线程所共享;实例变量在同一个客户端的多个线程中是共享的,在不同客户端是各自独立的。
在Single并发+PerSession实例中,实例变量不会产生并发冲突,而静态变量可能会在不同的客户端之间产生并发冲突,在同一客户端多线程间不会产生并发冲突。

3.ServiceBehavior(ConcurrencyMode = ConcurrencyMode.Single,InstanceContextMode = InstanceContextMode.Single) --Single并发与Single实例模型

WCF足迹6:并发1 - Tony - Go ahead!

《图3》
对于Single实例模型,多个客户端对应一个服务实例,这样就容易发生多个客户端同时请求一个服务进行处理,产生并发问题。当采取Single并发模式时,多个客户端的并发访问问题会得到解决,因为Single并发模式会使多个客户端请求进行排队,依次处理每个客户端的请求。
Single并发模式在Single实例模型中,每次只能处理一个调用请求,会对系统的吞吐量带来很大的影响

服务端代码:
    [ServiceContract]
    public interface ISingleSingle
    {
        [OperationContract]
        int GetValue1();
        [OperationContract]
        int GetValue2();
    }
    [ServiceBehavior(InstanceContextMode = InstanceContextMode.Single, ConcurrencyMode = ConcurrencyMode.Single)]
    public class SingleSingle : ISingleSingle
    {
        private int InstanceVariable = 0;
        private static int StaticVariable = 0;
        public int GetValue1()
        {
            return ++InstanceVariable;
        }

        public int GetValue2()
        {
            return ++StaticVariable;
        }
    }

客户端代码:
    class SingleSingle
    {
        private static SRSingleSingle.SingleSingleClient client = new Client.SRSingleSingle.SingleSingleClient();
        public static void Main(string[] args)
        {

            for (int i = 0; i < 10; i++)
            {
                Thread thread = new Thread(new ThreadStart(DoWork));
                thread.Start();

                while (!thread.IsAlive) ;
            }
        }
        public static void DoWork()
        {
            Console.WriteLine("线程" + Thread.CurrentThread.GetHashCode() + ":/t实例变量的值:" +client.GetValue1());
            Console.WriteLine("线程" + Thread.CurrentThread.GetHashCode() + ":/t静态变量的值:" +client.GetValue2());
        }
    }

    
运行结果:

WCF足迹6:并发1 - Tony - Go ahead!

《图3-1》
由于我们把实例模型设为Single,那所有客户端所有线程调用同一服务实例,故这些线程共享InstanceVariable实例变量,由于并发模式是Single模式,所以不会对此变量产生并发冲突。而对于StaticVariable静态变量,而言也是所有客户端线程所共享的,但这里的静态变量StaticVariable不会产生并发冲突,因为只有一个服务实例来操作此静态变量。

4.ServiceBehavior(ConcurrencyMode = ConcurrencyMode.Reentrant,InstanceContextMode = InstanceContextMode.PerCall) --Reentrant并发与PerCall实例模型

WCF足迹6:并发1 - Tony - Go ahead!

《图4》
对于PerCall实例模型,如果采用Single的并发模式时,在实现回调的时候会出现问题:当服务在处理客户请求的时候是处于锁定状态的,当回调的时候,在未解除锁定的状态下又去调用客户端,在客户端回调执行完成后,想再回到服务端的时候发现服务端被自己锁定无法进入,产生等待超时的异常,如下图左侧。
这就像在我们参加认证考试一样,考生进入考场打开电脑进行考试,相当于每个考生独占一台电脑。假设在考试过程中某考生出去打了个电话,那当它再回到考场想继续考试时,会被监考老师阻止,因为他违返了考场规则,无法再使用那台考试的电脑继续考试。而该考生的那台机器还被未交卷,仍被该考生占用,无法被他人使用。这样就出现了“有去无回”情况,即等待超时。
要解决这个问题,我们可以采用Reentrant并发模式,Reentrant模式依然是单线程模式,只是它允许客户端加调用能再回到服务端继续执行。
接上面的例子来说,就相当于考生在出去打电话的时候跟监考老师打个招乎,然后由巡考老师陪同一起出去打电话,在回来的时候,由巡考老师把学员带回考场交给监考老师继续考试。这就实现了“有去有回”的可重入模式了。

示例设计思路:
客端使用多线程的方式连续向服务端发出三个调用请求。服务端使用PerCall实例模型和Reentrant并发模式,在ServiceMethod方法中实现对客户端的回调操作。在客户端回调代码中只做了5秒的休眠时间。
为了能够说明问题,在客户端发出三个调用请求前后分别显示一下时间;在服务端执行回调操作前后分别显示一下时间。

服务端代码:
    public interface IReentrantPerCallCallBack
    {
        [OperationContract]
        void ClientMethod();
    }
    [ServiceContract(CallbackContract = typeof(IReentrantPerCallCallBack))]
    public interface IReentrantPerCall
    {
        [OperationContract]
        void ServiceMethod();
    }
    [ServiceBehavior(ConcurrencyMode = ConcurrencyMode.Reentrant, InstanceContextMode = InstanceContextMode.PerCall)]
    public class ReentrantPerCall : IReentrantPerCall
    {
        private IReentrantPerCallCallBack callback = OperationContext.Current.GetCallbackChannel<IReentrantPerCallCallBack>();
        public void ServiceMethod()
        {
            Debug.WriteLine("服务器端 " + this.GetHashCode() + ": 准备开始客户端回调......" + DateTime.Now.ToString("hh:mm:ss ms"));
           callback.ClientMethod();
            Debug.WriteLine("服务器端 " + this.GetHashCode() + ":客户端回调结束。" + DateTime.Now.ToString("hh:mm:ss ms"));

        }
    } 

客户端代码:
    class ReentrantPerCall:IReentrantPerCallCallback
    {
        private static ReentrantPerCall instance = new ReentrantPerCall();
        private static InstanceContext context = new InstanceContext(instance);
        private static SRReentrantPerCall.ReentrantPerCallClient client = new ReentrantPerCallClient(context);

        public void ClientMethod()
        {
            Thread.Sleep(5000);
        }
        public static void Main(string[] args)
        {
           for (int i = 0; i < 3; i++)
            {
                Thread thread = new Thread(new ThreadStart(DoWork));
                thread.Start();

            }
            
            Console.ReadLine();
            client.Close();
        }
        public static void DoWork()
        {
            Console.WriteLine("客户端线程" + Thread.CurrentThread.GetHashCode() + "发起调用......" + DateTime.Now.ToString("hh:mm:ss ms"));
            client.ServiceMethod();
            Console.WriteLine("客户端线程" + Thread.CurrentThread.GetHashCode() + "调用结束......" + DateTime.Now.ToString("hh:mm:ss ms"));

        }
    }

运行结果:

WCF足迹6:并发1 - Tony - Go ahead!

《图4-1》
从图中的结果我们看出
客户端:多线程发起调用时间是一样的,即向服务端发出的请求几乎是同一时间的。而线程结束的时间差别很大,之间几乎相差5秒。
服务端:确实有三个服务实例响应客户端的三个线程。
但从其发起调用和调用结束的时间来看,这三个服务实例并非同时发起对客户端的回调,而是有严格的先后次序。
按PerCall的实例模型来看,每个线程在服务端都对应独立的实例,这些服务都是并行处理的,既然客户端发出的请求几乎是同时的,那服务端就应当立马根据线程的请求生成三个服务实例,实现对客户端回调,然后重入服务端,最后结束调用。上面的代码运行效果与理论分析明显不一致。
这是为“调用模型”产生的不一致性。
前面我们谈过,调用模型有三种:“请求-响应”(默认)、“One-Way”和“Duplex”。
这里我们没有明确指出采用哪种调用模式,因此为默认使用“请求-响应”方式。而“请求-响应”模式,客户端必须等服务端“响应”完成上一次“请求”后才能发出下一步“请求”。因此虽然客户端使用多线程方式来调用服务,但最后的执行结果仍然表现出顺序处理。要想使服务端能够并行处理客户端请求的话,那我们就不能使用“请求-响应”的调用模式,我们可以使用One-Way的方式来调用服务。
因此我们可以把服务端的ServiceMethod方法契约修正如下:
    [OperationContract(IsOneWay=true)]
    void ServiceMethod();

其余的代码不做改变,虽然只是修了一下调用方式,但运行结果与之前并一样。

WCF足迹6:并发1 - Tony - Go ahead!

《图4-2》
通过运行结果我们看到
客户端:多线程的发起调用的时间是一样的,结束调用的时间也是一样的,并且发起调用和结束调用之间并没有时间延迟,这说明客户端多线程各自独立地向服务端发出调用成功。
服务端:确实在服务端产生三个实例处理客户端调用,并且这三个服务实例回调客户端的时间是相同的,但客户端回调结束的时间相差太大有明显的5秒延迟。
根据上面的经验,我们可以猜到这5秒的延迟是在服务端回调客户端的过程中引起的。回调过程实际上相当于客户端与服务端换了个位置,由服务端来调用客户端。由于回调契约中的方法契约ClientMethod没有明确设置调用方式,所以也是默认“请求-响应”的调用方式,如果回调没有完成,则下个回调只能处于阻塞状态。因此导致服务端的回调虽然同时发起,但结束回调的时间却差别很大。
下面我们在上面的基础上再试着把回调契约中的ClientMethod方法契约声明为One-Way
    [OperationContract(IsOneWay=true)]
    void ClientMethod();

其余的代码不做改变,运行结果:

WCF足迹6:并发1 - Tony - Go ahead!

《图4-3》
从上图我们可以看到
客户端:多线程异步调用
服务端:多线程异步调用,中间再没有发生延迟情况。

话又说回来,这种回调契约IsOneWay=true调用的方式却并不是ConcurrencyMode = ConcurrencyMode.Reentrant的并发模式。
上面的例子,我们是用一个代理的多个线程调用WCF服务,下面我们把客户端代码修改一下,实现多个代理用各自的线程调用WCF服务。为了能明确表达Reentrant并发模式,我们服务端方法契约的IsOneWay=true去掉。
服务代码:
    public interface IReentrantPerCallCallBack
    {
        [OperationContract]
        void ClientMethod();
    }
   [ServiceContract(CallbackContract = typeof(IReentrantPerCallCallBack))]
    public interface IReentrantPerCall
    {
        [OperationContract]
        void ServiceMethod();
    }
    [ServiceBehavior(ConcurrencyMode = ConcurrencyMode.Reentrant, InstanceContextMode = InstanceContextMode.PerCall)]
    public class ReentrantPerCall : IReentrantPerCall
    {
        private IReentrantPerCallCallBack callback = OperationContext.Current.GetCallbackChannel<IReentrantPerCallCallBack>();
        public void ServiceMethod()
        {
            Debug.WriteLine("服务器端 " + this.GetHashCode() + ": 准备开始客户端回调......" + DateTime.Now.ToString("hh:mm:ss ms"));
            callback.ClientMethod();
            Debug.WriteLine("服务器端 " + this.GetHashCode() + ":客户端回调结束。" + DateTime.Now.ToString("hh:mm:ss ms"));

        }
    }

    服务端的方法契约仍然是“请求-响应”模式。
    
客户代码:
    class ReentrantPerCall:IReentrantPerCallCallback
    {
        public void ClientMethod()
        {
            Thread.Sleep(5000);
        }
        public static void Main(string[] args)
        {
            for (int i = 0; i < 3; i++)
            {
                Thread thread = new Thread(new ThreadStart(DoWork));
                thread.Start();

            }
            Console.ReadLine();
        }
        public static void DoWork()
        {
            ReentrantPerCall instance = new ReentrantPerCall();
            InstanceContext context = new InstanceContext(instance);
            SRReentrantPerCall.ReentrantPerCallClient client = new ReentrantPerCallClient(context);

           Console.WriteLine("客户端线程" + Thread.CurrentThread.GetHashCode() + "发起调用......" + DateTime.Now.ToString("hh:mm:ss ms"));
            client.ServiceMethod();
            Console.WriteLine("客户端线程" + Thread.CurrentThread.GetHashCode() + "调用结束......" + DateTime.Now.ToString("hh:mm:ss ms"));
            client.Close();

        }
    }

    代理类对象不再是以成员变量的形式存在,而是在线程中被实例化,线程调用结束后会关闭代理类对象。这样相当于多个客户端代理同时向服务端发送请求。
    

运行结果:

WCF足迹6:并发1 - Tony - Go ahead!

《图4-4》
从图中可以看出,在客户端三个服务被同时发出,调用结束后几乎同时返回。在服务端,也几乎同时向客户端发出回调,客户端在经过5秒的延迟后也几乎同时返回。休现了实例之间的无关。