【问题标题】:Is WCF's DataContractSerilaizer thread safe?WCF 的 DataContractSerilaizer 线程安全吗?
【发布时间】:2012-10-12 23:08:09
【问题描述】:

我一直在将一个相当大的系统从 Remoting 转换为 WCF,一切似乎都运行良好,除了我们经常遇到以下异常:“System.InvalidOperationException:集合已修改;枚举操作可能无法执行。”我没有任何运气来追踪它,因为它只在有数百个调用通过时才会发生,我只能假设这是因为一个对象在被序列化时正在被修改。

所有类都使用:[DataContract(IsReference=true)]

使用远程处理时没有类似的异常,所以我想知道是否有人在 WCF 中遇到过类似的问题,或者可以让我知道它可能是序列化程序——在这种情况下,我假设我必须自己编写序列化程序在必要时执行locks(这是我希望避免的一项重大任务)。

以下是堆栈跟踪:

WCF Error: at System.Collections.Generic.List1.Enumerator.MoveNextRare() at WriteArrayOfLineToXml(XmlWriterDelegator , Object , XmlObjectSerializerWriteContext , CollectionDataContract ) at System.Runtime.Serialization.CollectionDataContract.WriteXmlValue(XmlWriterDelegator xmlWriter, Object obj, XmlObjectSerializerWriteContext context) at System.Runtime.Serialization.XmlObjectSerializerWriteContext.InternalSerialize(XmlWriterDelegator xmlWriter, Object obj, Boolean isDeclaredType, Boolean writeXsiType, Int32 declaredTypeID, RuntimeTypeHandle declaredTypeHandle) at System.Runtime.Serialization.XmlObjectSerializerWriteContext.InternalSerializeReference(XmlWriterDelegator xmlWriter, Object obj, Boolean isDeclaredType, Boolean writeXsiType, Int32 declaredTypeID, RuntimeTypeHandle declaredTypeHandle) at WriteLineGroupToXml(XmlWriterDelegator , Object , XmlObjectSerializerWriteContext , ClassDataContract ) at System.Runtime.Serialization.ClassDataContract.WriteXmlValue(XmlWriterDelegator xmlWriter, Object obj, XmlObjectSerializerWriteContext context) at System.Runtime.Serialization.XmlObjectSerializerWriteContext.SerializeWithoutXsiType(DataContract dataContract, XmlWriterDelegator xmlWriter, Object obj, RuntimeTypeHandle declaredTypeHandle) at System.Runtime.Serialization.XmlObjectSerializerWriteContext.InternalSerialize(XmlWriterDelegator xmlWriter, Object obj, Boolean isDeclaredType, Boolean writeXsiType, Int32 declaredTypeID, RuntimeTypeHandle declaredTypeHandle) at System.Runtime.Serialization.XmlObjectSerializerWriteContext.InternalSerializeReference(XmlWriterDelegator xmlWriter, Object obj, Boolean isDeclaredType, Boolean writeXsiType, Int32 declaredTypeID, RuntimeTypeHandle declaredTypeHandle) at WriteLineToXml(XmlWriterDelegator , Object , XmlObjectSerializerWriteContext , ClassDataContract ) at System.Runtime.Serialization.ClassDataContract.WriteXmlValue(XmlWriterDelegator xmlWriter, Object obj, XmlObjectSerializerWriteContext context) at System.Runtime.Serialization.XmlObjectSerializerWriteContext.SerializeAndVerifyType(DataContract dataContract, XmlWriterDelegator xmlWriter, Object obj, Boolean verifyKnownType, RuntimeTypeHandle declaredTypeHandle, Type declaredType) at System.Runtime.Serialization.XmlObjectSerializerWriteContext.SerializeWithXsiTypeAtTopLevel(DataContract dataContract, XmlWriterDelegator xmlWriter, Object obj, RuntimeTypeHandle originalDeclaredTypeHandle, Type graphType) at System.Runtime.Serialization.DataContractSerializer.InternalWriteObjectContent(XmlWriterDelegator writer, Object graph, DataContractResolver dataContractResolver) at System.Runtime.Serialization.DataContractSerializer.InternalWriteObject(XmlWriterDelegator writer, Object graph, DataContractResolver dataContractResolver) at System.Runtime.Serialization.XmlObjectSerializer.WriteObjectHandleExceptions(XmlWriterDelegator writer, Object graph, DataContractResolver dataContractResolver) at System.Runtime.Serialization.XmlObjectSerializer.WriteObject(XmlDictionaryWriter writer, Object graph) at System.ServiceModel.Dispatcher.DataContractSerializerOperationFormatter.SerializeParameterPart(XmlDictionaryWriter writer, PartInfo part, Object graph) at System.ServiceModel.Dispatcher.DataContractSerializerOperationFormatter.SerializeParameter(XmlDictionaryWriter writer, PartInfo part, Object graph) at System.ServiceModel.Dispatcher.DataContractSerializerOperationFormatter.SerializeBody(XmlDictionaryWriter writer, MessageVersion version, String action, MessageDescription messageDescription, Object returnValue, Object[] parameters, Boolean isRequest) at System.ServiceModel.Dispatcher.OperationFormatter.SerializeBodyContents(XmlDictionaryWriter writer, MessageVersion version, Object[] parameters, Object returnValue, Boolean isRequest) at System.ServiceModel.Dispatcher.OperationFormatter.OperationFormatterMessage.OperationFormatterBodyWriter.OnWriteBodyContents(XmlDictionaryWriter writer) at System.ServiceModel.Channels.BodyWriter.WriteBodyContents(XmlDictionaryWriter writer) at System.ServiceModel.Channels.BodyWriterMessage.OnWriteBodyContents(XmlDictionaryWriter writer) at System.ServiceModel.Channels.Message.OnWriteMessage(XmlDictionaryWriter writer) at System.ServiceModel.Channels.Message.WriteMessage(XmlDictionaryWriter writer) at System.ServiceModel.Channels.BufferedMessageWriter.WriteMessage(Message message, BufferManager bufferManager, Int32 initialOffset, Int32 maxSizeQuota) at System.ServiceModel.Channels.BinaryMessageEncoderFactory.BinaryMessageEncoder.WriteMessage(Message message, Int32 maxMessageSize, BufferManager bufferManager, Int32 messageOffset) at System.ServiceModel.Channels.FramingDuplexSessionChannel.EncodeMessage(Message message) at System.ServiceModel.Channels.FramingDuplexSessionChannel.OnSend(Message message, TimeSpan timeout) at System.ServiceModel.Channels.OutputChannel.Send(Message message, TimeSpan timeout) at System.ServiceModel.Dispatcher.DuplexChannelBinder.DuplexRequestContext.OnReply(Message message, TimeSpan timeout) at System.ServiceModel.Channels.RequestContextBase.Reply(Message message, TimeSpan timeout) at System.ServiceModel.Dispatcher.ImmutableDispatchRuntime.Reply(MessageRpc& rpc)

【问题讨论】:

  • 对于它的价值,错误,“System.InvalidOperationException:集合已修改;枚举操作可能无法执行。”很少与线程有关。一般是因为集合在枚举过程中被修改过,和foreach (var item in list) list.Remove(item);一样难以重现。
  • 你的枚举是什么?如果我的枚举使用的资源在我完成序列化之前就被释放了,我也会遇到同样的错误。
  • @Kirk,如果是这种情况,堆栈跟踪不会表明它是在序列化程序中完成的,因此是 WCF 中的一个错误吗?我相对确定我们的代码不会在任何地方这样做,因为我自己过去曾多次这样做,所以我(终于)意识到要提防它
  • @twreid 不幸的是,我花了很多时间来追踪它,所以我不知道枚举在哪里。但从堆栈跟踪来看,在我看来,它是通过 List 循环的序列化程序。我们没有明确地处理任何东西,但是垃圾收集器是否有可能在序列化过程中处理对象? (抱歉,对 gc 不太了解)

标签: c# .net multithreading wcf thread-safety


【解决方案1】:

确实,使用DataContractSerializer 可以轻松重现此错误。它不是指DataContractSerializer 的线程安全,它是关于某些集合的线程安全,用于您的数据合约:

[DataContract]
public class C
{
    [DataMember]
    public List<int> Values { get; set; }
}

class Program
{
    static void Main(string[] args)
    {
        var c = new C
        {
            Values = new List<int>()
        };

        var serializer = new DataContractSerializer(typeof(C));

        Task
            .Factory
            .StartNew(() => 
            {
                while (true)
                {
                    Console.WriteLine("Trying to add new item.");
                    c.Values.Add(DateTime.Now.Millisecond);
                }
            }, 
            TaskCreationOptions.LongRunning);

        Task
            .Factory
            .StartNew(() =>
            {
                while (true)
                {
                    using (var stream = new MemoryStream())
                    {
                        Console.WriteLine("Trying to serialize.");
                        serializer.WriteObject(stream, c);
                    }
                }
            },
            TaskCreationOptions.LongRunning);

        Console.ReadLine();
    }

在很短的执行时间之后,您将获得具有类似堆栈跟踪的 IOE:

   at System.ThrowHelper.ThrowInvalidOperationException(ExceptionResource resource)    at System.Collections.Generic.List`1.Enumerator.MoveNextRare()    at System.Collections.Generic.List`1.Enumerator.MoveNext()    at WriteArrayOfintToXml(XmlWriterDelegator , Object , XmlObjectSerializerWriteContext , CollectionDataContract )    at System.Runtime.Serialization.CollectionDataContract.WriteXmlValue(XmlWriterDelegator xmlWriter, Object obj, XmlObjectSerializerWriteContext context)

看起来您正在继续修改一些共享数据,这些数据同时被序列化。您可以打开 WCF 跟踪(请参阅this 问题)以找出导致此错误的操作,并仔细查看此操作正在使用哪些数据。

然后,根据当前服务行为的InstanceContextModeConcurrencyMode属性的值,可以选择走哪条路:

  • 要么使用某种锁定;
  • 或使用任何线程安全的集合;
  • 或改变服务行为;
  • 或更改服务本身(例如,使其无状态)。

【讨论】:

  • 感谢您的示例和详细回答。大多数服务是 InstanceContextMode.PerCall 和 ConcurrencyMode.Mulitple,所以我可能会使用线程安全集合(我不知道),这取决于 WCF 跟踪向我显示的内容——在我的困惑中,我完全忘记了跟踪,尽管没有您的说明,但仍然不会知道最佳解决方案。谢谢!
  • @user1766568:我希望,您不会将每个集合都替换为其线程安全的类似物,因为这是通往地狱的道路。
【解决方案2】:

如果丹尼斯的假设是正确的,那么解决这个问题的最简单方法是复制集合并通过网络发送副本。此时,在序列化期间是否修改原始内容并不重要

【讨论】:

  • 感谢您的回复。可能是最简单的,但对象图可能很深,所以我不确定在这种特殊情况下它会是最快的。我将返回一个可能有一个集合的对象,或者有一个带有集合的对象集合等。所以我想我可能不得不使用线程安全的集合。
  • 这不是假设。 :) 您可以编译代码示例以确保它。
  • 我并不是说您的代码没有说明问题 - 我所说的只是因为您的代码具有相同的症状并不意味着它与 OP 的原因相同。但是,如果您的代码是相同的原因,那么复制是解决问题的一种干净方法
猜你喜欢
  • 2011-06-05
  • 1970-01-01
  • 2013-12-23
  • 1970-01-01
  • 2020-04-15
  • 2018-07-18
  • 2011-07-04
  • 2014-04-26
  • 2012-11-30
相关资源
最近更新 更多