5.ServiceBehavior(ConcurrencyMode = ConcurrencyMode.Reentrant,InstanceContextMode = InstanceContextMode.PerSession) --Reentrant并发与PerSession实例模型
《图5》
对于PerSession的实例模型,每个客户端拥有一个服务实例,如果该客户端采用多线程模式与服务端交互,那这个客户端的所有线程共享一个实例。而Reentrant并发模式,依然是个单线程的模式,即某时刻只能有一个线程调用服务实例,其余的线程处于等待队列中。但是当某线程回调客户端操作时,该线程就不再锁定该服务实例,等待队列中的下个线程将锁定服务实例进行调用。当回调客户端的线程执行完客户端调用后再回到服务端时,它将进入线程等待队列中重新排队等待。
在“Reentrant并发+PerSession实例”模式中关于“调用方式(请求-响应、OneWay和Duplex)”对运行结果的影响,与上面“Reentrant并发+PerCall实例”模式中的“调用方式”对结果的影响是一样的。在这里我们不再逐一演示,大家可以跟据上面的讲述自己编写代码来验证。
下面我们依然把服务契约中的方法契约调用设置为IsOneWay=true。代码如下:
服务端代码:
public interface IReentrantPerSessionCallBack
{
[OperationContract]
void ClientMethod();
}
[ServiceContract(CallbackContract = typeof(IReentrantPerSessionCallBack), SessionMode = SessionMode.Required)]
public interface IReentrantPerSession
{
[OperationContract(IsOneWay=true)]
void ServiceMethod();
}
[ServiceBehavior(ConcurrencyMode = ConcurrencyMode.Reentrant, InstanceContextMode = InstanceContextMode.PerSession)]
public class ReentrantPerSession : IReentrantPerSession
{
private IReentrantPerSessionCallBack callback = OperationContext.Current.GetCallbackChannel<IReentrantPerSessionCallBack>();
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"));
}
}
在服务端代码中,我们没有把回调方法契约设为IsOneWay=true。
客户端代码:
public class ReentrantPerSession:SRReentrantPerSession.IReentrantPerSessionCallback
{
private static ReentrantPerSession instance = new ReentrantPerSession();
private static InstanceContext context = new InstanceContext(instance);
private static SRReentrantPerSession.ReentrantPerSessionClient client = new Client.SRReentrantPerSession.ReentrantPerSessionClient(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() + "发起调用......");
client.ServiceMethod();
Console.WriteLine("客户端线程" + Thread.CurrentThread.GetHashCode() + "调用结束....../n");
}
}
客户端中在Main函数中,依然使用同一个服务代理对象,采用多线程向服务端发出调用。在回调方法中先休眠5秒后再“重入”服务端。
运行效果:
《图5-1》
从图中我们看到
客户端中我们使用同一个代理对象的多线程技术向服务端发送请求,在这里我启用了两个客户端。
而服务器端一共产生了两个实例,每个实例代表一个客户端的调用。在每个服务对象中,我们可以看到三个客户端回调的时间基本相同,而客户端回调返回的时间却相隔5秒钟。
关于三个回调时间基本相同,并不代表服务端使用多线程同时对客户端进行三次回调,Reentrant并发模式仍然是单线程模式。原因是,当客户端三次请求(A,B,C)到达服务端后,A请求将锁定服务实例,并调用服务,B请求和C请求将在等待队列中排队。在A请求处理过程中进行回调时,A请求会暂时释放对服务实例的锁定,此时队列中的B请求会锁定服务,进行调用。当B请求处理回调时,B请求也会暂时释入服务实例锁定,此时C请求会锁定服务进行调用......。当A回调结束后会重入服务实例,并在等待队列的尾部排队等待后续处理。
关于客户端返回时间相隔5秒种,原因我们上面已经说过,即回调契约是“请求-响应”模式,只有收到上次调用的“响应”才能进行下次“请求”。
同样我们再把代码修改一下,使用多个代理用各自的线程调用WCF服务,来模拟多用户来调用服务实例。
服务端代码:
public interface IReentrantPerSessionCallBack
{
[OperationContract]
void ClientMethod();
}
[ServiceContract(CallbackContract = typeof(IReentrantPerSessionCallBack), SessionMode = SessionMode.Required)]
public interface IReentrantPerSession
{
//[OperationContract(IsOneWay=true)]
[OperationContract]
void ServiceMethod();
}
[ServiceBehavior(ConcurrencyMode = ConcurrencyMode.Reentrant, InstanceContextMode = InstanceContextMode.PerSession)]
public class ReentrantPerSession : IReentrantPerSession
{
private IReentrantPerSessionCallBack callback = OperationContext.Current.GetCallbackChannel<IReentrantPerSessionCallBack>();
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"));
}
}
在服务端代码中,ServiceMethod方法契约仍然使用“请求响应”的调用模式。
客户端代码:
public class ReentrantPerSession:SRReentrantPerSession.IReentrantPerSessionCallback
{
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()
{
ReentrantPerSession instance = new ReentrantPerSession();
InstanceContext context = new InstanceContext(instance);
SRReentrantPerSession.ReentrantPerSessionClient client = new Client.SRReentrantPerSession.ReentrantPerSessionClient(context);
Console.WriteLine("客户端线程" + Thread.CurrentThread.GetHashCode() + "发起调用......");
client.ServiceMethod();
Console.WriteLine("客户端线程" + Thread.CurrentThread.GetHashCode() + "调用结束....../n");
client.Close();
}
}
在客户端,在线程内部使用不同的代理向服务端发起调用。
运行效果:
《图5-2》
从图中我们可以看出来,每个客户端只很服务端发起一个线程的调用,这种调用模式能够实现对服务端的并行调用,并且也不会产生并发冲突,其实这种用法相当于“Reentrant并发模式+PerCall实例模型”。
6.ServiceBehavior(ConcurrencyMode = ConcurrencyMode.Reentrant,InstanceContextMode = InstanceContextMode.Single) --Reentrant并发与Single实例模型
《图6》
对于“Single实例模型+Reentrant并发模式”与“PerSession实例模型+Reentrant并发模式”有些相似,是单线程重入处理模式。如果调用方式是“请求响应”方式时,如果是同一个代理的多个线程会受到阻塞,如果是多个代理则不同代理会并行调用;如果是OneWay调用模式,则不会出现任阻塞情况。
上面“Single实例模型+Reentrant并发模式”与“PerSession实例模型+Reentrant并发模式”这两个模型就与“去银行排队”的例子很相似。
服务端代码:
public interface IReentrantSingleCallBack
{
[OperationContract]
void ClientMethod();
}
[ServiceContract(CallbackContract = typeof(IReentrantSingleCallBack))]
public interface IReentrantSingle
{
[OperationContract]
void ServiceMethod(string clientThreadID);
}
[ServiceBehavior(ConcurrencyMode = ConcurrencyMode.Reentrant, InstanceContextMode = InstanceContextMode.Single)]
public class ReentrantSingle : IReentrantSingle
{
public void ServiceMethod(string clientThreadID)
{
IReentrantSingleCallBack callback = OperationContext.Current.GetCallbackChannel<IReentrantSingleCallBack>();
Debug.WriteLine("服务器端 " + this.GetHashCode() + ",客户端线程" + clientThreadID + ": 准备开始客户端回调......" + DateTime.Now.ToString("hh:mm:ss ms"));
callback.ClientMethod();
Debug.WriteLine("服务器端 " + this.GetHashCode() + ",客户端线程" + clientThreadID + ":客户端回调结束。" + DateTime.Now.ToString("hh:mm:ss ms"));
}
}
客户端代码:
class ReentrantSingle : IReentrantSingleCallback
{
private static ReentrantSingle instance = new ReentrantSingle();
private static InstanceContext context = new InstanceContext(instance);
private static SRReentrantSingle.ReentrantSingleClient client = new Client.SRReentrantSingle.ReentrantSingleClient(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() + "发起调用......");
client.ServiceMethod(Thread.CurrentThread.ManagedThreadId.ToString()+"-"+Guid.NewGuid().ToString());
Console.WriteLine("客户端线程" + Thread.CurrentThread.GetHashCode() + "调用结束....../n");
}
}
运行效果
《图6-1》
从图中我们看出两个客户端的六个线程的确调用服务端的同一个实例,用蓝色和绿色标出来的执行结果分别是不同客户端的线程回调情况。我们可以看出两个客户端之间是并行运行的,而同一个客户端的三个线程则是排队调用的--因为用的是“请求-响应”的调用模式。
到目前为至我们研究完了PerCall和Reentrant两种并发模式,这两种并发模式都是单线程调用,每个调用都会独占其服务,因此并不会对服务实例产生并发调用的冲突。
7.ServiceBehavior(ConcurrencyMode = ConcurrencyMode.Multiple,InstanceContextMode = InstanceContextMode.PerCall) --Multiple并发与PerCall实例模型
《图7》
对于Multiple并发模型,应当是多线程的并发访问模式,但对于PerCall实例模型中,每个线程与一个独立的服务实例进行交互,所以一般不会产生并发冲突。但如果服务实例中使用了静态变量或全局数据缓存的时候,在多线程操作这些共享资源时需要手动编写同步代码,以确保这些共享资源不会发生并发冲突。
服务器端代码:
[ServiceContract]
public interface IMultiplePerCall
{
[OperationContract(IsOneWay = true)]
void TestService(string clientThreadID);
}
[ServiceBehavior(ConcurrencyMode=ConcurrencyMode.Multiple, InstanceContextMode=InstanceContextMode.PerCall)]
public class MultiplePerCall : IMultiplePerCall
{
private int _InstanceState = 0;
private static int _StaticState = 0;
public void TestService(string clientThreadID)
{
Random rand = new Random();
while (true)
{
_InstanceState++;
_StaticState = _InstanceState;
Thread.Sleep(rand.Next(500));
Debug.WriteLine("实例" + this.GetHashCode().ToString() + ": " + clientThreadID.ToString() + " ;状态的值是:" + _StaticState.ToString() + " - " + _InstanceState.ToString());
}
}
}
在这段代码中,我们对服务端的调用依然使用OneWay方式。在类中我们设置了两个成员变量,一个是实例变量,一个是静态变量。由于我们使用PerCall实例模型,每个线程对应一个服务实例,对于实例变量应当不会产生并发冲突的问题,但对于静态变量,由于使用的是Multiple并发模型,有可能会发生多个实例对象同时修改同一个静态变量的问题。
为了演示这种并发效果我们使用了一个死循环:
while (true)
{
_InstanceState++;
_StaticState = _InstanceState;
Thread.Sleep(rand.Next(500));
Debug.WriteLine("实例" + this.GetHashCode().ToString() + ": " + clientThreadID.ToString() + " ;状态的值是:" + _StaticState.ToString() + " - " + _InstanceState.ToString());
}
在循环中,我们使用实例变量的值来修改静态变量,并在修改完后线程休眠500毫秒,以让出处理器让其它线程执行处理,然后再显示我们刚才赋值的结果。
如果没有产生并发冲突,应当显示实例变量和静态变量相等。如果产生了并发冲突,那在当前实例执行完_StaticState = _InstanceState后,_StaticState变量的值又会被其它服务实例修改,最后会导实例变量和静态变量的值不一致的结果。
客户端代码:
class MultiplePerCall
{
private static SRMultiplePerCall.MultiplePerCallClient client = new Client.SRMultiplePerCall.MultiplePerCallClient();
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()
{
client.TestService(Thread.CurrentThread.ManagedThreadId.ToString());
}
}
一个代理实例使用三个线程调用服务。
运行结果:
《图7-1》
从运行结果上来看,
服务端产生了三个实例58366981,56140151和37916227。
每个服务的实例变量的值(横线右边的状态值)永远是不重复递增,即实例变量不会产生并发冲突。
实例变量(横线右边的状态值)和静态变量(横线左边的状态值)不一致,说明在_StaticState = _InstanceState后,静态变量又被其它服务实例修改过,即产生了并发冲突。
要想防止多个服务实例对静态变量修改操作产生并发冲突,需要我们手写代码对它进行同步保护。
服务端代码做如下修改
public class MultiplePerCall : IMultiplePerCall
{
......
while (true)
{
lock (typeof(MultiplePerCall))
{
....
}
}
}
}
使用lock语句段,把变量修改与显示的代码锁定在一起,防止其实服务实例的并发修改冲突。
8.ServiceBehavior(ConcurrencyMode = ConcurrencyMode.Multiple,InstanceContextMode = InstanceContextMode.PerSession) --Multiple并发与PerSession实例模型
《图8》
“Multiple并发+PerSession实例模型”会对每个客户端的多线程请求指派一个服务实例进行处理,多线程的请求同一个服务时,同样并不会自动锁定该服务,对于服务中的数据进行修改的时候也需要我们手动编写同步代码进行保护。对于不同的客户端虽然对应不同的服务实例,但这些服务实例有可能共享静态数据或全局缓存数据,如果需要对这些数据进行操作的时候也需要手动编写代码进行同步保护。
服务端代码:
[ServiceContract(SessionMode= SessionMode.Required)]
public interface IMultiplePerSession
{
[OperationContract(IsOneWay=true)]
void TestService(string clientThreadID);
}
[ServiceBehavior(ConcurrencyMode = ConcurrencyMode.Multiple, InstanceContextMode = InstanceContextMode.PerSession)]
public class MultiplePerSession : IMultiplePerSession
{
private int _InstanceState = 0;
private int _StateValue = 0;
public void TestService(string clientThreadID)
{
Random rand = new Random();
while (true)
{
_InstanceState++;
Thread.Sleep(rand.Next(500));
_StateValue = _InstanceState;
Debug.WriteLine("实例" + this.GetHashCode().ToString() + ": " + clientThreadID.ToString() + " 状态值:" + _InstanceState + " - " + _StateValue);
}
}
}
在这时我们仍使用OneWay的调用模式,代码设计的思想与上面中设计的思想相似,只是休眠的位置不是在赋完值后休眠,而是在赋值之前休眠。如果产生并发的话,在休眠的话,就会把我刚赋好的值覆盖掉,出现变量的值不连续或数据重复的情况。
客户端代码:
class MultiplePerSession
{
private static SRMultiplePerSession.MultiplePerSessionClient client = new Client.SRMultiplePerSession.MultiplePerSessionClient();
public static void Main8(string[] args)
{
for (int i = 0; i < 3; i++)
{
Thread thread = new Thread(new ThreadStart(DoWork));
thread.Start();
}
Console.ReadLine();
}
public static void DoWork()
{
client.TestService(Thread.CurrentThread.ManagedThreadId.ToString());
}
}
这里我们同时启动两个客户端,每个客户端使用三个线程向服务端发起调用。
运行结果:
《图8-1》
从图中我们看出,服务端的确有两个服务在运行(42931033和20974680),这两个服务实例分别出现了一次数据重复。并且是同一服务实例内的数据重复,即说明了同一客户端的多线程调用发生了并发冲突。
防止并发冲突的同步锁定代码:
lock (this)
{
...
}
9.ServiceBehavior(ConcurrencyMode = ConcurrencyMode.Multiple,InstanceContextMode = InstanceContextMode.Single) --Multiple并发与Single实例模型
《图9》
这种并发模型与实例模型的匹配算是多线程并发操作的一个极佳搭配。在Single实例模型中,所有客户端的所有线程共享一个服务实例,这样可以很好地解决多个客户端的不同线程间的状态共享的问题。可是这种实例模型的吞吐量会小于PerCall实例模型的吞吐量,因此我们可以使用Multiple并发模型来增加其吞吐量,可以允许多个客户端线程对服务实例同时调用。但这样也最容易对一些共享数据和共享资源引起并发冲突,所以需要我们手动编写代码对共享数据和共享资源进行并发保护。
服务端代码:
[ServiceContract]
public interface IMultipleSingle
{
[OperationContract( IsOneWay=true)]
void TestService(string clientThreadID);
}
[ServiceBehavior(ConcurrencyMode = ConcurrencyMode.Multiple, InstanceContextMode = InstanceContextMode.Single)]
public class MultipleSingle : IMultipleSingle
{
private int _InstanceState = 0;
public void TestService(string clientThreadID)
{
Random rand = new Random();
while (true)
{
_InstanceState++;
Thread.Sleep(rand.Next(10));
Trace.WriteLine(string.Format("实例{0},线程{1}:状态量的值是 {2};", this.GetHashCode(), clientThreadID, _InstanceState));
}
}
}
在这里我只使用了实例变量,没有使用静态变量,多线程的客户端会对该服务实例产生并发冲突。在为实例变量赋完值后,我休眠一段时间,再显示赋值的结果,结果会在休眠的这段时间内其它线程来修改变量的值导致重复数据的产生。
客户端代码:
class MultipleSingle
{
private static SRMultipleSingle.MultipleSingleClient client = new Client.SRMultipleSingle.MultipleSingleClient();
public static void Main(string[] args)
{
for (int i = 0; i < 10; i++)
{
Thread thread = new Thread(new ThreadStart(DoWork));
thread.Start();
}
Console.ReadLine();
}
public static void DoWork()
{
client.TestService(Thread.CurrentThread.ManagedThreadId.ToString());
}
}
运行结果:
《图9-1》
从上面图中看出,服务端只有一个服务实例,三个线程同时运行,产生并发冲突。
总结上面的内容,增加系统吞吐量可以提升服务的效率。但影响吞吐量的因素比较多,如:实例模型、并发模型和调用模型等。
限流模型也会对吞吐量造成影响。限流是为了控制客户端或服务端相关资源的数量,以使服务处于最佳的运行状态。
三、限流
可以配置服务行为的serviceThrottling属性来对WCF服进行限流。
MaxConcurrentCalls:限制客户端发起的并发请求数量 – 缺省为16
MaxConcurrentInstances:限制服务端服务实例的数量。
MaxConcurrentSessions:限制活动会话数量-缺省值10