一、背景
1 class Program 2 { 3 static void Main(string[] args) 4 { 5 using (ServiceHost host = new ServiceHost(typeof(FileImp))) 6 { 7 host.AddServiceEndpoint(typeof(IFile), new WSHttpBinding(), "http://127.0.0.1:9999/FileService"); 8 if (host.Description.Behaviors.Find<ServiceMetadataBehavior>() == null) 9 { 10 ServiceMetadataBehavior behavior = new ServiceMetadataBehavior(); 11 behavior.HttpGetEnabled = true; 12 behavior.HttpGetUrl = new Uri("http://127.0.0.1:9999/FileService/metadata"); 13 host.Description.Behaviors.Add(behavior); 14 } 15 host.Opened += delegate 16 { 17 Console.WriteLine("FileService已经启动,按任意键终止服务!"); 18 }; 19 host.Open(); 20 Console.Read(); 21 } 22 } 23 } 24 25 class FileImp : IFile 26 { 27 static byte[] _fileContent = new byte[1024 * 8]; 28 29 public byte[] GetFile(string fileName) 30 { 31 int loginID = OperationContext.Current.IncomingMessageHeaders.GetHeader<int>("LoginID", string.Empty); 32 Console.WriteLine(string.Format("调用者ID:{0}", loginID)); 33 return _fileContent; 34 } 35 }
2、客户端代码,循环调用GetFile操作,在调用前给消息头添加一些登录信息。另外为了避免垃圾回收机制执行的不确定性对内存增长的干扰,在每次调用完毕后,强制启动垃圾回收机制,对所有代进行垃圾回收,确保增长的内存都是可到达,无法对其进行回收。
1 class Program 2 { 3 static void Main(string[] args) 4 { 5 int callCount = 0; 6 int loginID = 0; 7 while (true) 8 { 9 using (ChannelFactory<IFile> channelFactory = 10 new ChannelFactory<IFile>(new WSHttpBinding(), "http://127.0.0.1:9999/FileService")) 11 { 12 IFile fileProxy = channelFactory.CreateChannel(); 13 using (fileProxy as IDisposable) 14 { 15 //OperationContext.Current = new OperationContext(fileProxy as IContextChannel); 16 OperationContextScope scope = new OperationContextScope(fileProxy as IContextChannel); 17 var loginIDHeadInfo = MessageHeader.CreateHeader("LoginID", string.Empty, ++loginID); 18 OperationContext.Current.OutgoingMessageHeaders.Add(loginIDHeadInfo); 19 byte[] fileContent = fileProxy.GetFile(string.Empty); 20 } 21 } 22 GC.Collect();//强制启动垃圾回收 23 Console.WriteLine(string.Format("调用次数:{0}", ++callCount)); 24 } 25 } 26 }
二、分析排查
![](https://i-blog.csdnimg.cn/blog_migrate/85b095a2fef32b2b344fc0b139e9acff.png)
三、问题解决
1 class Program 2 { 3 static void Main(string[] args) 4 { 5 int callCount = 0; 6 int loginID = 0; 7 while (true) 8 { 9 using (ChannelFactory<IFile> channelFactory = 10 new ChannelFactory<IFile>(new WSHttpBinding(), "http://127.0.0.1:9999/FileService")) 11 { 12 IFile fileProxy = channelFactory.CreateChannel(); 13 using (fileProxy as IDisposable) 14 { 15 //OperationContext.Current = new OperationContext(fileProxy as IContextChannel); 16 using (OperationContextScope scope = new OperationContextScope(fileProxy as IContextChannel)) 17 { 18 var loginIDHeadInfo = MessageHeader.CreateHeader("LoginID", string.Empty, ++loginID); 19 OperationContext.Current.OutgoingMessageHeaders.Add(loginIDHeadInfo); 20 } 21 byte[] fileContent = fileProxy.GetFile(string.Empty); 22 } 23 } 24 GC.Collect();//强制启动垃圾回收 25 Console.WriteLine(string.Format("调用次数:{0}", ++callCount)); 26 } 27 } 28 }
四、问题根源
OperationContextScope为什么能持有大量的OperationContext引用?从CLR Profiler工具获取的结果中可以看到OperationContextScope对象通过其内部OperationContextScope对象来持有大量OperationContext对象引用,可以推断该类应该有一个OperationContextScope类型的字段。下面看一下OperationContextScope类的源码。
1 public sealed class OperationContextScope : IDisposable 2 { 3 [ThreadStatic] 4 static OperationContextScope currentScope; 5 6 OperationContext currentContext; 7 bool disposed; 8 readonly OperationContext originalContext = OperationContext.Current; 9 readonly OperationContextScope originalScope = OperationContextScope.currentScope; 10 readonly Thread thread = Thread.CurrentThread; 11 12 public OperationContextScope(IContextChannel channel) 13 { 14 this.PushContext(new OperationContext(channel)); 15 } 16 17 public OperationContextScope(OperationContext context) 18 { 19 this.PushContext(context); 20 } 21 22 public void Dispose() 23 { 24 if (!this.disposed) 25 { 26 this.disposed = true; 27 this.PopContext(); 28 } 29 } 30 31 void PushContext(OperationContext context) 32 { 33 this.currentContext = context; 34 OperationContextScope.currentScope = this; 35 OperationContext.Current = this.currentContext; 36 } 37 38 void PopContext() 39 { 40 if (this.thread != Thread.CurrentThread) 41 throw DiagnosticUtility.ExceptionUtility.ThrowHelperError(new InvalidOperationException(SR.GetString(SR.SFxInvalidContextScopeThread0))); 42 43 if (OperationContextScope.currentScope != this) 44 throw DiagnosticUtility.ExceptionUtility.ThrowHelperError(new InvalidOperationException(SR.GetString(SR.SFxInterleavedContextScopes0))); 45 46 if (OperationContext.Current != this.currentContext) 47 throw DiagnosticUtility.ExceptionUtility.ThrowHelperError(new InvalidOperationException(SR.GetString(SR.SFxContextModifiedInsideScope0))); 48 49 OperationContextScope.currentScope = this.originalScope; 50 OperationContext.Current = this.originalContext; 51 52 if (this.currentContext != null) 53 this.currentContext.SetClientReply(null, false); 54 } 55 }
当前的上下文对象由线程唯一的静态字段currentScope持有,其实例字段originalScope保持前一上下文对象的引用,如果使用完毕后不结束当前上下文范围,就会一直嵌套下去,导致所有OperationContext对象都保持可到达,垃圾回收机制无法进行回收,从而使得内存持续增长,直到内存溢出。
五、总结
类似OperationContextScope,TranscationScope以XXXScope结尾的类都可以看作Context+ContextScope的设计方式(参考Artech大神的博文:Context+ContextScope——这是否可以看作一种设计模式?),用于在同一范围内共享同一事物或对象。在使用这类上下文对象的时候,确保使用using关键字来使得上下文范围边界可控。