AppDomain、序列化和 System.Threading.Timer 问题

Posted

技术标签:

【中文标题】AppDomain、序列化和 System.Threading.Timer 问题【英文标题】:AppDomain, Serialization and System.Threading.Timer issue 【发布时间】:2011-11-15 17:50:55 【问题描述】:

我有一个使用 CreateInstanceAndUnwrap 在 AppDomain 中创建的类。该类包含一个 System.Threading.Timer。

我遇到的问题是,当类被实例化时,计时器的回调方法似乎看不到类实例的正确值。

我有下面的示例代码来说明问题:

库类

using System;
using System.Threading;

namespace Library

    [Serializable]
    public class Class1
    
        public Class1()
        
            Started = false;

            _Timer = new Timer(TimerMethod);
        

        public bool Started  get; set; 

        private readonly Timer _Timer;
        private string _Message;
        private string _TimerMessage;

        public bool Start()
        
            Started = true;

            _Message = string.Format("Class1 says Started = 0", Started);
            _TimerMessage = "Timer message not set yet";

            _Timer.Change(1000, 1000);

            return Started;
        

        public string GetMessage()
        
            // _TimerMessage is never set by TimerMethod when this class is created within an AppDomain
            return string.Format("0, 1", _Message, _TimerMessage);
        

        public void TimerMethod(object state)
        
            // Started is always false here when this class is created within an AppDomain
            _TimerMessage = string.Format("Timer says Started = 0 at 1", Started, DateTime.Now);
        
    

消费类

using System;
using System.Windows.Forms;
using Library;

namespace GUI

    public partial class Form1 : Form
    
        public Form1()
        
            InitializeComponent();

            var appDomainSetup = new AppDomainSetup
            
                ApplicationName = "GUI",
                ApplicationBase = AppDomain.CurrentDomain.BaseDirectory
            ;

            _AppDomain = AppDomain.CreateDomain(appDomainSetup.ApplicationName,
                                                AppDomain.CurrentDomain.Evidence,
                                                appDomainSetup);

            _Class1 = _AppDomain.CreateInstanceAndUnwrap("Library", "Library.Class1") as Class1;
        

        private readonly AppDomain _AppDomain;
        private readonly Class1 _Class1;

        private void button1_Click(object sender, EventArgs e)
        
            _Class1.Start();
            MessageBox.Show(_Class1.GetMessage());
        

        private void button2_Click(object sender, EventArgs e)
        
            MessageBox.Show(_Class1.GetMessage());
        
    

当上面的代码运行时,GetMessage() 总是返回:

Class1 说 Started = True,尚未设置计时器消息

但是,如果我改成上面表格的构造函数来创建Class1的本地实例,

        public Form1()
        
            InitializeComponent();

            _Class1 = new Class1();
        

GetMessage() 返回预期的消息:

Class1 说 Started = True,Timer 说 Started = True 于 2011 年 11 月 15 日下午 12:34:06

我搜索了 Google、MSDN 和 SO,但没有找到任何专门针对 AppDomain、Serialization 和 System.Threading.Timer 组合的信息。我也找不到任何关于 TimerCallback 为何无法引用实例化 Timer 的类的本地成员的信息。

【问题讨论】:

当您将调试器放在“_TimerMessage = string.Format("Timer says Started = 0 at 1", Started, DateTime.Now);"代码执行到那里了吗? 让 Class1 从 MarshalByRefObject 派生来解决问题。 @slfan,在我的实际课程中,我还有其他不可序列化的类(例如 XElement)。对于这些类,我在运行时收到一个 SerializationException,上面写着“Type 'System.Xml.Linq.XElement' ...未标记为可序列化。”我本来希望 Timer 类也是如此。 @kurtnelle,是的,我可以在计时器回调例程中设置一个断点,当我检查 Started 时,它是 false。 【参考方案1】:

这很可能是由“按值编组”(您的班级)和“按引用编组”(很可能是您想要的)之间的差异引起的。如果类不是从 MarshalByRefObject 派生的,那么它在远程处理期间的行为类似于值类型,这意味着您在通信的每一端都获得对象的 副本。如果类型从 MarshalByRefObject 派生,那么您在未实例化对象的一侧获得 proxy,并且该一侧将能够调用另一 AppDomain 中实例上的方法。

链接:

MarshalByRefObject - http://msdn.microsoft.com/en-us/library/system.marshalbyrefobject.aspx

MSDN 杂志中跨应用程序域调用期间的生命周期管理文章 - 下载 2003 年 12 月发行的 MSDN magazine(您可能需要取消阻止文件属性中的内容)或使用 Web 存档链接 Managing the Lifetime of Remote .NET Objects with Leasing and Sponsorship

【讨论】:

我对我的示例代码进行了建议的更改,并且成功了。但是,当我对我的生产代码进行相同的更改时,我收到了我的消费者类的 SerializationException。在示例中,消费者类是一个表单,但在我的生产代码中,消费者类是另一个包含许多不可序列化成员的类。我决定也尝试从 MarshalByRefObject 降级我的消费者类,这似乎已经成功了。谢谢。 另外,感谢 LifeTime 管理参考!这将非常有帮助。 杂志链接不再有效 - 根据***.com/a/25315226/155892,它位于 2003 年 12 月 期,但可以从同一页面以 CHM 格式下载。【参考方案2】:

TimerMethod 中的评论说:

// Started is always false here when this class is created within an AppDomain

但是你的输出表明Started 是真的。

这是什么?

实际上,当您创建本地实例时它可以工作,我有点惊讶。 Class1 构造函数创建计时器并给它一个回调,但它没有设置间隔或到期时间,这意味着计时器不会触发。

当您调用Start 时,计时器会被初始化,但会给出 1 秒的到期时间。 Start 返回,您致电 GetMessage 获取消息。但是,如果您在计时器有机会执行其回调之前调用GetMessage,您将获得您所描述的行为。

如果您在调用 Start 和调用 GetMessage 之间延迟 1 秒,我想您会发现问题在于……时间:您试图在计时器之前收到消息有机会设置它。请尝试以下方法进行验证:

private void button1_Click(object sender, EventArgs e)

    _Class1.Start();
    Thread.Sleep(1000);
    MessageBox.Show(_Class1.GetMessage());

或者,我想您可以在延迟几秒钟后再次尝试单击该按钮。

【讨论】:

button1_Click 返回的消息不是问题。该消息是预期的,因为计时器尚未触发。几秒钟后来自 button2_Click 的消息显示了问题。当在 AppDomain 中创建 Class1 时,在 TimerMethod always 中设置断点显示 Start 为 false。

以上是关于AppDomain、序列化和 System.Threading.Timer 问题的主要内容,如果未能解决你的问题,请参考以下文章

从 appdomain 中解包不可序列化的类

AppDomain CreateInstanceAndUnwrap:类型未标记为可序列化

获取对象的 AppDomain

在不同 AppDomain 上的序列化分类之间同步属性/字段

将派生类型序列化为 AppDomain 上的基本类型

从不同的 AppDomain 调用 SignalR 方法