您的位置:知识库 » .NET技术

警惕匿名方法造成的变量共享

作者: Jeffrey Zhao  来源: 博客园  发布时间: 2009-03-13 23:29  阅读: 2053 次  推荐: 0   原文链接   [收藏]  
[1] 匿名方法的特点,引出案例
[2] 分析问题原因
[3] 解决问题

分析原因

  要发现问题所在,我们必须了解匿名方法在.NET环境中的实现方式。

  .NET中本没有什么“匿名方法”,也没有类似的新特性。“匿名方法”完全是由编译器施展的魔法,它会将匿名方法中需要访问的所有成员一起包含在闭包中,确保所有的成员调用都符合.NET标准。例如在文章第一节中的第2个示例,实际上由编译器处理之后就变成了如下的样子(自然字段名经过“友好化”处理):

class TestClass
{
    ...

    private sealed class AutoGeneratedHelperClass
    {
        public TestClass m_testClassInstance;
        public int m_index;

        public void Action(string m)
        {
            this.m_index++;
            this.m_testClassInstance.Print(m);
        }
    }

    public void TestAfterCompiled()
    {
        AutoGeneratedHelperClass helper = new AutoGeneratedHelperClass();
        helper.m_testClassInstance = this;
        helper.m_index = 0;

        string[] messages = new string[] { "Hello", "World" };
        Action<string> action = new Action<string>(helper.Action);
        Array.ForEach(messages, action);

        Console.WriteLine(helper.m_index);
    }
}

  由此就可以看出编译器是如何实现一个闭包的:

  • 编译器自动生成一个私有的内部辅助类,并将其设为sealed,这个类的实例将成为一个闭包对象。
  • 如果匿名方法需要访问方法的参数或局部变量,那么该参数或局部变量将“升级”成为辅助类中的公有Field字段。
  • 如果匿名方法需要访问类中的其它方法,那么辅助类中将保存类的当前实例。

  值得一提的是,在实际情况下以上三点理论都皆可能不满足。在某些特别简单的情况下(例如匿名方法中完全不涉及局部变量和其他方法),编译器只会简单生成一个静态的方法来构造一个委托实例,因为这样可以获得更好的性能。

  对于之前的案例,我们现在也将它进行一番改写,这样便可“避免”使用匿名对象,也可以清楚地展现出问题原因:

private class AutoGeneratedClass
{
    public List<Item> m_batchItems;

    public void WaitCallback(object o)
    {
        DataContext db = new DataContext();
        db.Items.InsertAllOnSubmit(this.m_batchItems);
        db.SubmitChanges();
    }
}

static void Process()
{ 
    var helper = new AutoGeneratedClass();
    helper.m_batchItems = new List<Item>();

    foreach (var item in ...)
    {
        helper.m_batchItems.Add(item);

        if (helper.m_batchItems.Count > 1000)
        {
            ThreadPool.QueueUserWorkItem(helper.WaitCallback);
            helper.m_batchItems = new List<Item>();
        }
    }
}

  编译器会自动生成一个AutoGeneratedClass类,并且在Process方法中使用这个类的实例来代替原来的batchItems局部变量。同样,交给ThreadPool的委托对象也从匿名方法变成了AutoGeneratedClass实例的公有方法。因此线程池每次调用的便是该实例的WaitCallback方法。

  现在问题应该一目了然了吧?每次把委托交给线程池之后,线程池并不会立即执行,而会保留到合适的时间再进行。而WaitCallback方法在执行时,它会读取m_batchItems这个Field字段“当前”所引用的对象。而与此同时,Process方法已经“抛弃”了原本我们要提交的数据,因此会引起提交到数据库中数据的丢失。同时,在准备每批次数据的过程中,很有可能会发起两次数据提交,两个线程提交同样一批Item时,就抛出了所谓“莫名其妙”的异常。

0
0

.NET技术热门文章

    .NET技术最新文章

      最新新闻

        热门新闻