Hello World
Spiga

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

2009-03-13 09:03 by 老赵, 27226 visits

匿名方法

匿名方法是.NET 2.0中引入的高级特性,“匿名”二字说明它可以把实现内联地写在一个方法中,从而形成一个委托对象,而不用有明确地方法名,例如:

static void Test()
{
    Action<string> action = delegate(string value)
    {
        Console.WriteLine(value);
    };

    action("Hello World");
}

但是匿名方法的关键并不仅于“匿名”二字。其最强大的特性就在于匿名方法形成了一个闭包,它可以作为参数传递到另一个方法中去,但同时也能访问方法的局部变量和当前类中的其它成员。例如:

class TestClass
{
    private void Print(string message)
    {
        Console.WriteLine(message);
    }

    public void Test()
    {
        string[] messages = new string[] { "Hello", "World" };
        int index = 0;

        Action<string> action = (m) =>
        {
            this.Print((index++) + ". " + m);
        };

        Array.ForEach(messages, action);
        Console.WriteLine("index = " + index);
    }
}

如上所示,在TestClass的Test方法中,action委托调用了同在TestClass类中的私有方法Print,并对Test方法中的局部变量index进行了读写。在加上C# 3.0中Lambda表达式的新特性,匿名方法的使用得到了极大的推广。不过,如果使用不当,匿名方法也容易造成难以发现的问题。

问题案例

某位兄弟最近在一个简单的数据导入程序,主要工作是从文本文件中读取数据,进行分析和重组,然后写入数据库。其逻辑大致如下:

static void Process()
{
    List<Item> batchItems = new List<Item>();
    foreach (var item in ...)
    {
        batchItems.Add(item);

        if (batchItems.Count > 1000)
        {
            DataContext db = new DataContext();
            db.Items.InsertAllOnSubmit(batchItems);
            db.SubmitChanges();

            batchItems = new List<Item>();
        }
    }
}

每次从数据源中读取数据后,添加到batchItems列表中,当batchItems满1000条时便进行一次提交。这段代码功能运行正常,可惜时间卡在了数据库提交上。数据的获取和处理很快,但是提交一次就要花较长时间。于是想想,数据提交和数据处理不会有资源上的冲突,那么就把数据提交放在另外一个线程上进行处理吧!于是,使用ThreadPool来改写代码:

static void Process()
{ 
    List<Item> batchItems = new List<Item>();
    foreach (var item in ...)
    {
        batchItems.Add(item);

        if (batchItems.Count > 1000)
        {
            ThreadPool.QueueUserWorkItem((o) =>
            {
                DataContext db = new DataContext();
                db.Items.InsertAllOnSubmit(batchItems);
                db.SubmitChanges();
            });                    

            batchItems = new List<Item>();
        }
    }
}

现在,我们将数据提交操作交给ThreadPoll执行,当线程池中有额外线程时,就会发起数据提交操作。而数据提交操作不会阻塞数据处理,因此按照那位兄弟的意图,数据会不断进行处理,最后只要等待所有数据库提交完成就可以了。思路很好,可惜运行时发现,原本(不利用多线程时)运行正常的代码,如今会“莫名其妙”地抛出异常。更为奇怪的是,数据库中的数据出现了丢失的情况:处理了并“提交”了一百万条数据,但是数据库里却少了一部分。于是对着代码左看右看,百思不得其解。

您看出问题原因来了吗?

分析原因

要发现问题所在,我们必须了解匿名方法在.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时,就抛出了所谓“莫名其妙”的异常。

解决问题

找到了问题所在,解决起来自然轻而易举:

private class WrapperClass
{
    private List<Item> m_items;

    public WrapperClass(List<Item> items)
    {
        this.m_items = items;
    }

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

static void Process()
{
    List<Item> batchItems = new List<Item>();
    foreach (var item in ...)
    {
        batchItems.Add(item);

        if (batchItems.Count > 1000)
        {
            ThreadPool.QueueUserWorkItem(
                new WrapperClass(batchItems).WaitCallback);

            batchItems = new List<Item>();
        }
    }
}

这里我们明确地准备一个封装类,用它来保留我们需要提交的数据。而每次提交时则使用保留好的数据,自然不会发生不该有的“数据共享”,从而避免了错误的发生1

总结

匿名方法是强大的,但是也会造成一些令人难以察觉的陷阱。对于使用匿名方法创建的委托,如果不会立即同步执行,并且其中使用了方法的局部变量,那么您就需要对其留个心眼了。因为此时“局部变量”事实上已经由编译器转变成一个自动类的实例上的Field字段,而这个字段将被当前方法和委托对象共享。如果您在创建了委托对象之后还会修改共享的“局部变量”,那么请再三确认这样做符合您的意图,而不会造成问题。

此类问题也不光会出现在匿名方法中。如果您使用Lambda表达式创建了一个表达式树,其中也用到了一个“局部变量”,那么表达式树在解析或执行时同样也会获取“当前”的值,而不是创建表达式树时的值。

这也是为什么Java中的内联写法——匿名类——如果要共享方法内的“局部变量”,则必须将变量使用final关键字来修饰:这样这个变量只能在声明时赋值,避免了后续的“修改”可能会造成的“古怪问题”。

 

注1:一个更简洁的解决方案可以参考29楼overred兄弟的回复。

Creative Commons License

本文基于署名 2.5 中国大陆许可协议发布,欢迎转载,演绎或用于商业目的,但是必须保留本文的署名赵劼(包含链接),具体操作方式可参考此处。如您有任何疑问或者授权方面的协商,请给我留言

Add your comment

103 条回复

  1. Astar
    *.*.*.*
    链接

    Astar 2009-03-13 00:18:00

    敬佩你的减肥方法!

  2. Aaron Wu
    *.*.*.*
    链接

    Aaron Wu 2009-03-13 00:22:00

    今天刚碰到有人问我匿名方法,这下老赵的文章出来了,可以分享个链接给他就结了~

  3. works guo
    *.*.*.*
    链接

    works guo 2009-03-13 00:55:00

    好文。。学习

  4. 老赵
    admin
    链接

    老赵 2009-03-13 01:06:00

    @Astar
    谢谢支持

  5. ~Javascript~[未注册用户]
    *.*.*.*
    链接

    ~Javascript~[未注册用户] 2009-03-13 01:06:00

    应当说这是“闭包”必然的特性。跟是不是“匿名方法”没什么关系。

  6. 老赵
    admin
    链接

    老赵 2009-03-13 01:07:00

    @Aaron Wu
    发生这个错误的还是是个蛮强大的人,说明这个问题的确容易发生,某些情况下。

  7. 老赵
    admin
    链接

    老赵 2009-03-13 01:10:00

    --引用--------------------------------------------------
    ~Javascript~: 应当说这是“闭包”必然的特性。跟是不是“匿名方法”没什么关系。
    --------------------------------------------------------
    “AAA会引起BBB”,只能说明AAA是BBB的“充分条件”或“充要条件”,不是“必要条件”,因此我从头到底都没有说“这个问题产生的必然原因是匿名方法”。
    而且事实上,我也没有说,匿名方法“必然”会引起问题,因此连充分/充要条件都不是了,呵呵。

  8. ~Javascript~[未注册用户]
    *.*.*.*
    链接

    ~Javascript~[未注册用户] 2009-03-13 01:49:00

    @Jeffrey Zhao
    针对标题“警惕匿名方法‘造成’的变量共享”。
    我所要表达的意思是“这个问题‘并非’匿名方法造成的”,而是“闭包”的特性,同时匿名方法也不一定就应用了“闭包”,只要是应用了“闭包”,就存在这个问题。
    至于语言上的逻辑推论,嘿嘿嘿,并不打紧。看得懂的,自然也不会局限于匿名方法。
    PS:我是整晚咳个不停睡不着,真佩服你的精力,难道熬夜也是瘦身计划之一?

  9. 飞林沙
    *.*.*.*
    链接

    飞林沙 2009-03-13 04:35:00

    终于看懂了.......

  10. 非空
    *.*.*.*
    链接

    非空 2009-03-13 08:32:00

    for(int i = 0; i < 3; i++) {
    int x = i * 2 + 1;
    result[i] = delegate { Console.WriteLine(x); };
    }

    输出 1、3、5
    int x;

    for(int i = 0; i < 3; i++) {
    x = i * 2 + 1;
    result[i] = delegate { Console.WriteLine(x); };
    }
    输出5、5、5
    这也算是难察觉的问题吧

  11. 非空
    *.*.*.*
    链接

    非空 2009-03-13 08:34:00

    还有如果不是 batchItems = new List<Item>();
    这样而是batchItems[i]=XXXX;
    是不是显式声明的类中用来保存batchItems的字段也不管用了

  12. 老赵
    admin
    链接

    老赵 2009-03-13 09:06:00

    @~Javascript~
    一直熬夜的,体重从80kg到123kg再回到现在的86,所以看得出熬夜不是减肥或增肥的必要条件,hoho。

  13. 老赵
    admin
    链接

    老赵 2009-03-13 09:07:00

    @非空
    5、5、5什么的,难道和文章说的不是一个问题吗?

  14. 老赵
    admin
    链接

    老赵 2009-03-13 09:08:00

    @非空
    你的话没听懂,不过batchItems[i]自然就是替换掉了某个下标元素。
    总之,batchItem只有一个,而且被共享了便是。

  15. 苏飞
    *.*.*.*
    链接

    苏飞 2009-03-13 09:11:00

    呵呵今天是第一次来这里,不过早听你的大名,在视频 上有见识,呵呵,看了你的文章,正好解决了我的难题,谢谢!!!

  16. JimLiu
    *.*.*.*
    链接

    JimLiu 2009-03-13 09:12:00

    终于看懂了……

  17. 1-2-3
    *.*.*.*
    链接

    1-2-3 2009-03-13 09:16:00

    是不是也可以这样解决呢?
    static void Process()
    {
    List<Item> batchItems = new List<Item>();
    foreach (var item in ...)
    {
    batchItems.Add(item);

    if (batchItems.Count > 1000)
    {
    ThreadPool.QueueUserWorkItem((o) =>
    {
    DataContext db = new DataContext();
    db.Items.InsertAllOnSubmit(new List<Item>(batchItems)); // 变成局部变量
    db.SubmitChanges();
    });

    batchItems = new List<Item>();
    }
    }
    }

  18. 1-2-3
    *.*.*.*
    链接

    1-2-3 2009-03-13 09:19:00

    偶的方法似乎不行呀,看来是我想错了。

  19. 冰狼
    *.*.*.*
    链接

    冰狼 2009-03-13 09:21:00

    一个简单问题,讲得这么复杂。

  20. 老赵
    admin
    链接

    老赵 2009-03-13 09:21:00

    @1-2-3
    原来你也没有搞清楚,嘿嘿。

  21. Leon.w
    *.*.*.*
    链接

    Leon.w 2009-03-13 09:22:00

    还行 大体看得懂

  22. 老赵
    admin
    链接

    老赵 2009-03-13 09:25:00

    --引用--------------------------------------------------
    冰狼: 一个简单问题,讲得这么复杂。
    --------------------------------------------------------
    复杂吗?呵呵,要不您来讲一遍?

  23. 1-2-3
    *.*.*.*
    链接

    1-2-3 2009-03-13 09:26:00

    @Jeffrey Zhao
    嗯,问题的关键是new Wrap类的位置不同,而是不变量的问题。难道就只能手写个Wrap类么?好不爽的说。

  24. 老赵
    admin
    链接

    老赵 2009-03-13 09:26:00

    --引用--------------------------------------------------
    Leon.w: 还行 大体看得懂
    --------------------------------------------------------
    的确是简单的问题。

  25. Kain
    *.*.*.*
    链接

    Kain 2009-03-13 09:28:00

    batchItems.Add(item);

    if (batchItems.Count > 1000)
    {
    ThreadPool.QueueUserWorkItem((o) =>
    {
    DataContext db = new DataContext();
    db.Items.InsertAllOnSubmit(batchItems);
    db.SubmitChanges();
    });

    batchItems = new List<Item>();
    }

    1、一个线程向集合中加,一个线程枚举集合,这可以?不会报集合已经被修改的错误?
    2、b.Items.InsertAllOnSubmit(batchItems); 这种一次提交1000效率高不到哪里去。
    3、很多时候用匿名方法就是为了共享方法体里面的私有变量。

  26. 张蒙蒙
    *.*.*.*
    链接

    张蒙蒙 2009-03-13 09:33:00

    能保证每个线程分配的任务源是不同的就会没事了。

  27. 老赵
    admin
    链接

    老赵 2009-03-13 09:35:00

    --引用--------------------------------------------------
    张蒙蒙: 能保证每个线程分配的任务源是不同的就会没事了。
    --------------------------------------------------------
    没错,这里错误的原因就是不恰当的共享。

  28. 老赵
    admin
    链接

    老赵 2009-03-13 09:38:00

    @Kain
    1、文章不都说了有时会出现“莫名其妙”的异常?
    2、这不是文章重点,不必强求
    3、你说的没错,这是很强大的特性——不过我说的是“如果共享了那么就要注意”,哪里说过“不该共享”或“不适合共享”了?
    我怎么觉得有些兄弟们的逻辑总是让我那么难以理解啊……还是我的意思真那么难以理解?

  29. overred
    *.*.*.*
    链接

    overred 2009-03-13 09:52:00

    if

  30. 1-2-3
    *.*.*.*
    链接

    1-2-3 2009-03-13 09:56:00

    这样行不行?
    static void Main(string[] args)
    {
    Queue<int> q = new Queue<int>();

    for (int i = 0; i < 3; i++)
    {
    q.Enqueue(i);
    Thread t = new Thread(delegate()
    {
    //Thread.Sleep(new Random().Next(100, 1000));
    Console.WriteLine("i={0}, q={1}", i, q.Dequeue());
    });
    t.Start();
    }
    }
    输出:
    i=1, q=0
    i=2, q=1
    i=2, q=2

  31. 雪海飘香
    *.*.*.*
    链接

    雪海飘香 2009-03-13 09:56:00

    学习,谢谢分享

  32. 老赵
    admin
    链接

    老赵 2009-03-13 09:57:00

    @overred
    写得好!我居然忘了这个方式,呵呵。

  33. 老赵
    admin
    链接

    老赵 2009-03-13 10:02:00

    @1-2-3
    如果Queue是线程安全的,那么问题不大。

  34. 老赵
    admin
    链接

    老赵 2009-03-13 10:13:00

    @overred
    不错不错,不过你咋还动用dbg了,最后的结论也很奇怪啊。

  35. 1-2-3
    *.*.*.*
    链接

    1-2-3 2009-03-13 10:24:00

    @Jeffrey Zhao
    overred 的作法真是非常漂亮,应该写进Effective C#里面。

  36. 老赵
    admin
    链接

    老赵 2009-03-13 10:28:00

    @1-2-3
    哎,可惜trick的意味太浓。

  37. Nick Wang
    *.*.*.*
    链接

    Nick Wang 2009-03-13 10:32:00

    很好,很强大,很通俗,很易懂。

  38. 1-2-3
    *.*.*.*
    链接

    1-2-3 2009-03-13 10:40:00

    --引用--------------------------------------------------
    Jeffrey Zhao: @1-2-3

    哎,可惜trick的意味太浓。
    --------------------------------------------------------
    哈哈,我刚刚也在想要是MS在搞.net X.0的时候突发奇想做了个优化,觉得 tempItems 和 batchItems不是相同的东东嘛,直接把tempItems 给移除了,替换成了batchItems,那可就哭了

  39. 木野狐(Neil Chen)
    *.*.*.*
    链接

    木野狐(Neil Chen) 2009-03-13 11:04:00

    我觉得不一定要 wrapper class, 也可以在循环里面,在将要处理之前,copy 这些数据到一个新的 List 或者 Array 里面然后提交,就不会有这个问题。

  40. 老赵
    admin
    链接

    老赵 2009-03-13 11:05:00

    @木野狐(Neil Chen)
    嗯嗯,再循环里放到一个新的“临时变量”里也可以了。

  41. 重典
    *.*.*.*
    链接

    重典 2009-03-13 11:15:00

    为何我总是赶不 上沙发

  42. 韦恩卑鄙
    *.*.*.*
    链接

    韦恩卑鄙 2009-03-13 12:36:00

    确实经常发生 我也遇到过

  43. 飞林沙
    *.*.*.*
    链接

    飞林沙 2009-03-13 12:57:00

    请问29L的代码与您的有什么分别,看了好几遍,都感觉基本是一个意思啊

  44. 简单生活
    *.*.*.*
    链接

    简单生活 2009-03-13 12:59:00

    以前也遇到过这样的问题,后来写代码的时候就比较注意了,尽量不在循环中使用匿名方法。
    在循环中使用匿名方法时,匿名方法中不要使用会随循环变化的外部变量。如果一定要使用随循环变化的外部变量,可以在循环中重新声明这个变量并赋值。

    如果编译器够聪明的话,应该就不用这么费事了:)

  45. 飞林沙
    *.*.*.*
    链接

    飞林沙 2009-03-13 13:03:00

    哦哦哦 看明白了

  46. wuxiaoqqqq
    *.*.*.*
    链接

    wuxiaoqqqq 2009-03-13 13:06:00

    昨天刚好详细研究了JavsScript里面的闭包,对这个有很多的感触,今天老赵的文章就写了C#里面的匿名方法,以后要注意了。

  47. aa1[未注册用户]
    *.*.*.*
    链接

    aa1[未注册用户] 2009-03-13 13:19:00

    本来看了你的照片就有点倒胃口,可你换了N张全都是一样的遗像风格,你能不能。。。。。。

  48. 老赵
    admin
    链接

    老赵 2009-03-13 13:36:00

    --引用--------------------------------------------------
    简单生活: 以前也遇到过这样的问题,后来写代码的时候就比较注意了,尽量不在循环中使用匿名方法。
    在循环中使用匿名方法时,匿名方法中不要使用会随循环变化的外部变量。如果一定要使用随循环变化的外部变量,可以在循环中重新声明这个变量并赋值。

    如果编译器够聪明的话,应该就不用这么费事了:)
    --------------------------------------------------------
    但是有时候我就是要实现这种先声明后设值,编译器不应该Handle这些事情,我觉得现在的做法不错。

  49. 老赵
    admin
    链接

    老赵 2009-03-13 13:36:00

    --引用--------------------------------------------------
    aa1: 本来看了你的照片就有点倒胃口,可你换了N张全都是一样的遗像风格,你能不能。。。。。。
    --------------------------------------------------------
    没办法,哥哥就长这样了阿

  50. Anytao
    *.*.*.*
    链接

    Anytao 2009-03-13 13:51:00

    @木野狐(Neil Chen)
    同意Neil Chen的,这也是闭包的典型解决思路呀

  51. 朱乙
    *.*.*.*
    链接

    朱乙 2009-03-13 14:29:00

    顶你一下

  52. 王泽宾
    *.*.*.*
    链接

    王泽宾 2009-03-13 15:05:00

    老赵的这幅照片看起来颇有风尘感啊

  53. 老赵
    admin
    链接

    老赵 2009-03-13 15:37:00

    --引用--------------------------------------------------
    王泽宾: 老赵的这幅照片看起来颇有风尘感啊
    --------------------------------------------------------
    啥叫风尘感?

  54. DiryBoy
    *.*.*.*
    链接

    DiryBoy 2009-03-13 15:54:00

    自动生成的闭包没有标记为可序列化,在两个AppDomain之间CallBack想要传点其他参数就要用到文章的技巧自己打包了,加一个参数就要改一次,痛苦~

  55. Franz
    *.*.*.*
    链接

    Franz 2009-03-13 16:05:00

    闭包会造成很多诡异的问题。在给为老赵添加一个例子。更好玩。

    class Program
    {
    static void Main(string[] args)
    {
    int counter = 0;
    IEnumerable<int> values = Utilities.Generate(20, () => counter++);
    Console.WriteLine("Current Counter: {0}", counter);
    foreach (int num in values)
    Console.WriteLine(num);
    Console.WriteLine("Current Counter: {0}", counter);
    foreach (int num in values)
    Console.WriteLine(num);
    Console.WriteLine("Current Counter: {0}", counter);
    }
    }
    public static class Utilities
    {
    public static IEnumerable<T> Generate<T>(int num, Func<T> generator)
    {
    int index = 0; while (index++ < num)
    yield return generator();
    }
    }

  56. 韦恩卑鄙
    *.*.*.*
    链接

    韦恩卑鄙 2009-03-13 17:43:00

    我来吐槽
    照相那天 老赵太严肃了
    不就是台下美女比较多 眼神比较风骚 态度比较热情么 看把他羞的

  57. 老赵
    admin
    链接

    老赵 2009-03-13 18:18:00

    --引用--------------------------------------------------
    韦恩卑鄙: 我来吐槽
    照相那天 老赵太严肃了
    不就是台下美女比较多 眼神比较风骚 态度比较热情么 看把他羞的
    --------------------------------------------------------
    哪里哪里,我一点印象也没有

  58. 拓荒者
    *.*.*.*
    链接

    拓荒者 2009-03-13 18:40:00

    请老赵帮忙解释一下#59楼中提到的javascript中的怪现象吧!

  59. 老赵
    admin
    链接

    老赵 2009-03-13 18:44:00

    @拓荒者
    但是这也限制了一些使用方式啊,呵呵。

  60. rainnoless
    *.*.*.*
    链接

    rainnoless 2009-03-13 23:40:00

    匿名函数,我喜欢,闭包,是个很难理解的东西,呵呵。我说的是JavaScript里的,非C#中的,呵呵。
    这篇文章的效果就像春天的花儿——招蜂引蝶,哈哈。

  61. jilin[未注册用户]
    *.*.*.*
    链接

    jilin[未注册用户] 2009-03-14 13:40:00

    我觉得好像简单问题复杂化了,其实就是不同的线程引用了相同的对象,但执行上还有时差。还没看完的时候,我就在想,这样恐怕会有问题,似乎应该batchItems.Clone()一下(只是伪码,有没有这个Clone方法我不知道啊);后来29楼说的再赋值一下的方式我没试过,我只是下意识的觉得那样可能还是按引用传递

    是不是这样啊。。

  62. 老赵
    admin
    链接

    老赵 2009-03-14 14:35:00

    @jilin
    所以您还是没有理解,想当然了。
    1、Clone一下是没有用的。
    2、29楼的方法是可以解决问题得。

  63. airwolf2026
    *.*.*.*
    链接

    airwolf2026 2009-03-14 17:13:00

    总算看懂了些...
    helper.m_batchItems = new List<Item>();

    foreach (var item in ...)
    {
    helper.m_batchItems.Add(item);
    -----------------------------------------------------------------
    这个helper.m_batchItems在process方法执行完被丢弃,导致了有引用的'item'也被丢弃了?应该是这个意思吧?

    而29楼改写后,
    List<Item> tempItems = batchItems;
    则是保存了整个list<item>引用,因此在线程方法里面访问到的数据还是对的?

    是这样理解吧

  64. jilin[未注册用户]
    *.*.*.*
    链接

    jilin[未注册用户] 2009-03-14 22:07:00

    @Jeffrey Zhao
    看了老赵的回复,郁闷了一下,随后也就释怀了。
    自己写了一个实验程序,印证了一下我的想法,结果我是正确的。
    我与29楼所不同的只是如下一句话的区别:
    List<Item> clonedItems = CloneItems(batchItems); //我的做法
    List<Item> newItems = batchItems;//他的做法

    我猜想问题的实质就在于List<>的赋值操作相当于按值传递,是这样吗??老赵直接给解释一下吧。

  65. 老赵
    admin
    链接

    老赵 2009-03-14 22:11:00

    @jilin
    我以为你说是在委托里面进行Clone……因为其实不用Clone阿,29楼的方法就可以了,所以我猜你现在的理解还是有点问题……
    其实原因我文章里也已经说过了,你后面的猜测似乎是不正确的——当然前提是:我正确理解的了你的意思。要验证的话,可以借助.NET Reflector,呵呵。

  66. airwolf2026
    *.*.*.*
    链接

    airwolf2026 2009-03-14 22:56:00

    -_-!!!...看来俺的理解还是有问题的哈...

  67. jilin[未注册用户]
    *.*.*.*
    链接

    jilin[未注册用户] 2009-03-14 23:23:00

    @Jeffrey Zhao
    我做了实验,以前从没有仔细考虑过的问题,原对象在new了以后,不影响指向该原对象的引用的对象(不要笑话我啊~~)。
    我把我的理解说的明白些,看看是不是我的理解还是有问题,还是另有原因:
    线程池中的线程N在处理batchItems集合时,该集合可能被主线程修改,导致了不可预料的行为。

    在我的实验里,有时会报主键冲突错误,有时会报集合被修改无法枚举错误,由于我是用Hashtable来模拟,因此还有同步问题,加lock才行。


    多线程的代价,真是要综合成本、必要性等多方面因素来决定是否使用啊。。

  68. 老赵
    admin
    链接

    老赵 2009-03-15 14:46:00

    @overred
    似乎很多朋友还不习惯用.NET Reflector啊,很多都靠猜啊猜的,这可不好……

  69. 老赵
    admin
    链接

    老赵 2009-03-16 10:15:00

    @jilin
    别看IL啊,看C#,我是能不看IL就不看IL的,呵呵。

  70. overred
    *.*.*.*
    链接

    overred 2009-03-17 15:58:00

    @jilin
    单从CLR,抛开VS编译优化等问题看,
    List<Item> tempItems = batchItems;这句只是把tempItems 在堆栈上的指针指向batchItems在堆中的obj。

    但是这句放到类似闭包里就不同啦,编译器会进行干涉,正是此问题困惑拉你!
    在这点上我感觉Vs编译器有点欺骗人的感觉,不过这种情况不多。
    我一般从Jit编译后内存的数据形态来看,不过针对此问题可以在IL里很清晰的知道原因。

  71. overred
    *.*.*.*
    链接

    overred 2009-03-17 16:01:00

    @jilin
    呵呵,我的MSN:overred2005#163.com
    =_=

  72. overred
    *.*.*.*
    链接

    overred 2009-03-17 16:17:00

    @jilin
    您在72楼的例子中,sadly,
    private void button2_Click(object sender, EventArgs e)
    {
    Item item = new Item("1");
    Item item2 = item;
    item = new Item("2");
    MessageBox.Show(item2.ID);
    }

    中没有深克隆对象。
    从MSIL来看,当你Item item2 = item; 的时候,对应的
    L_000c: ldloc.0
    L_000d: stloc.1

    Ldloc 将指定索引处的局部变量加载到计算堆栈上。
    Stloc 从计算堆栈的顶部弹出当前值并将其存储到指定索引处的局部变量列表中。
    并不是newobj 。

  73. laowang[未注册用户]
    *.*.*.*
    链接

    laowang[未注册用户] 2009-03-25 11:05:00

    http://dev.csdn.net/article/26/26201.shtm
    技术文档都像佛法一样,该说的都说了,但是说的好晦涩难懂。

    谢谢楼主的文章和overred的回复

  74. yeml[未注册用户]
    *.*.*.*
    链接

    yeml[未注册用户] 2009-04-20 12:02:00

    这个问题并不新鲜,在java的内部匿名类里面一样有这样一个问题
    当内部匿名内的方法要使用调用者方法定义的变量或者参数时,这个变量或者参数应该定义为final

    原因是这样的,如果是单线程的情况的话,是不会有问题的,
    如果是多线程,如果不定义为final,那有空能有一种情况会发生:调用方法执行完后,如果此时vm调用了gc,那么,该方法内的变量会被GC掉,而此时可能闭包内的线程在gc后才调用的这个变量,那这个时候就会出空指针了。

    java的解决办法是把这个闭包用到的调用者方法的变量定义为final,这样这个变量就有了全局根了,GC的时候不会被清掉,从而保证不会出现无法预料的结果。

    。net的闭包跟这个问题是一样的。

  75. 老赵
    admin
    链接

    老赵 2009-04-20 12:06:00

    @yeml
    java里使用final是这个作用(全局根)?我不能理解,没听懂,为什么会被GC掉?
    C#就是最传统的做法,由编译器生成自动对象,然后把引用交给各线程,这样GC就和普通没什么两样,被引用的对象自然不会被GC。

  76. yeml[未注册用户]
    *.*.*.*
    链接

    yeml[未注册用户] 2009-04-20 12:14:00

    从这个问题可以看出,搞微软那块的人,好多基础都不扎实
    微软就是这样,什么都帮你弄好,你不用去学习基础的东西

  77. yeml[未注册用户]
    *.*.*.*
    链接

    yeml[未注册用户] 2009-04-20 12:17:00

    老赵

    java的final还有个用,就是一次赋值后,不能再赋值

    final如果定义类的话,这个类就不能被继承

    final等于 c#里的好几个东西:readonly, sealed

    方法里面的对象是放在 0代里面的,方法执行完,由于方法里面定义的变量不会再有引用指向这些变量,gc的时候会被清掉

  78. yeml[未注册用户]
    *.*.*.*
    链接

    yeml[未注册用户] 2009-04-20 12:18:00

    GC是按线程来的,别的线程引用了这个变量,GC是不管的

  79. yeml[未注册用户]
    *.*.*.*
    链接

    yeml[未注册用户] 2009-04-20 12:18:00

    简单来讲,GC是基于堆栈的

  80. 老赵
    admin
    链接

    老赵 2009-04-20 12:34:00

    --引用--------------------------------------------------
    yeml: 简单来讲,GC是基于堆栈的
    --------------------------------------------------------
    你的意思是GC是基于“栈”而不是基于“堆”的,你说的final的作用我都知道,只是我刚了解GC是线程独立的,这点和.NET不同。
    我比较感兴趣的是,Java的对象难道不是生成在堆上的吗?它把对象交给多个线程使用的时候难道不是复制引用吗?那么为什么回收一个线程的栈的时候会把对象也回收掉呢?

  81. yeml[未注册用户]
    *.*.*.*
    链接

    yeml[未注册用户] 2009-04-20 12:39:00

    老赵,你不用吃饭啊~~~·
    我找篇关于java的内部匿名类final变量的文章给你
    可能我理解有误,但是意思大约是这么个意思

  82. 老赵
    admin
    链接

    老赵 2009-04-20 12:39:00

    --引用--------------------------------------------------
    yeml: 从这个问题可以看出,搞微软那块的人,好多基础都不扎实
    微软就是这样,什么都帮你弄好,你不用去学习基础的东西
    --------------------------------------------------------
    这个逻辑我也一直不能接受。
    别人帮你做的多,你自己需要了解的少,并不代表你可以偷懒。
    你自己偷懒不学东西,为什么又要责怪别人让你太轻松了?
    这和小孩责怪父母对自己太好有什么区别,呵呵。
    工作轻松,反而可以让我有更多精力有选择性地关注根本的东西,不是吗?

  83. 老赵
    admin
    链接

    老赵 2009-04-20 12:40:00

    --引用--------------------------------------------------
    yeml: 老赵,你不用吃饭啊~~~&#183;
    我找篇关于java的内部匿名类final变量的文章给你
    可能我理解有误,但是意思大约是这么个意思
    --------------------------------------------------------
    谢谢阿。
    // 1点吃。:)

  84. yeml[未注册用户]
    *.*.*.*
    链接

    yeml[未注册用户] 2009-04-20 12:43:00

    你看看这个,这个是我收藏的,人家写的,我开始也是没考虑过为什么,有一天兴趣来了,才研究这个问题的。


    final变量定义: 变量一经初始化就不能指向其它对象。指向的存储地址不可修改,但指向的对象本身是可以修改的。

    先说final变量初始化:

    很多文章都这么说:其初始化可以在两个地方,一是其定义处,二是在构造函数中,两者只能选其一。
    胡说八道!
    final变量可以在任何可以被始化的地方被始化,但只能被初始化一次.一旦被初始化后就不能再次赋
    值(重新指向其它对象),作为成员变量一定要显式初始化,而作为临时变量则可以只定义不初始化(当然也不能引用)
    即使是作为一个类中的成员变量,也还可以在初始化块中初始化,所以"其初始化可以在两个地方,一是其定义处,
    二是在构造函数中,两者只能选其一"是错误的.


    作为成员变量时,final字段可以设计不变类,是不变类的一个必要条件但不是一个充要条件.至少可以保证字段不
    会以setXXX()这样的方式来改变.但无法保证字段本身不被修改(除非字段本身也是不变类);

    对于方法参数的final变量:
    对于方法参数的变量定义为final,90%以上的文章都说"当你在方法中不需要改变作为参数的对象变量时,明确使
    用final进行声明,会防止你无意的修改而影响到调用方法外的变量。"
    胡说八道!

    我不知道这个修改是说重新赋值还是修改对象本身,但无论是哪种情况,上面的说法都是错误的.
    如果是说重新赋值,那么:
    public static void test(int[] x){
    x = new int[]{1,2,3};
    }

    int[] out = new int[]{4,5,6};
    test(out);
    System.out.println(out[0]);
    System.out.println(out[1]);
    System.out.println(out[2]);
    调用test(out);无论如何也不会影响到外面变量out.你加不加final根本没有意义.final只会强迫方法内
    多声明一个变量名而已,即把x = new int[]{1,2,3};改成int y = new int[]{1,2,3}; 其它没有任何实际意义.
    如果说是修改对象本身:
    public static void test(final int[] x){
    x[0] = 100;
    }
    int[] out = new int[]{4,5,6};
    test(out);
    System.out.println(out[0]);
    难道你用final修饰就不可以修改了?所以说对于方法参数中final是为了不影响调用方法外的变量那是胡说八道的.

    那我们到底为什么要对参数加上final?其实对方法参数加final和方法内变量加上final的作用是相同的,即为了将它们
    传给内部类回调方法:

    abstract class ABSClass{
    public abstract void m();
    }

    现在我们来看,如果我要实现一个在一个方法中匿名调用ABSClass.应该:
    public static void test(String s){
    //或String s = "";
    ABSClass c = new ABSClass(){
    public void m(){
    int x = s.hashCode();

    System.out.println(x);

    }
    };
    //其它代码.
    }
    注意这里,一般而言,回调方法基本上是在其它线程中调用的.如果我们在上面的
    ABSClass c = new ABSClass(){
    public void m(){
    int x = s.hashCode();

    System.out.println(x);

    }
    };
    后面直接调用c.m();应该是没有意义的.但这不重要,重要的是只要有可能是在其它线程中调用,那我们就必须
    为s保存引用句柄.

    我们先来看GC工作原理,JVM中每个进程都会有多个根,每个static变量,方法参数,局部变量,当然这都是指引用类型.
    基础类型是不能作为根的,根其实就是一个存储地址.

    GC在工作时先从根开始遍历它引用的对象并标记它们,如此递归到最末梢,所有根都遍历后,没有被标记到的对象说明没
    有被引用,那么就是可以被回收的对象(有些对象有finalized方法,虽然没有引用,但JVM中有一个专门的队列引用它
    们直到finalized方法被执行后才从该队列中移除成为真正没有引用的对象,可以回收,这个与本主题讨论的无关,包括
    代的划分等以后再说明).这看起来很好.

    但是在内部类的回调方法中,s既不可能是静态变量,也不是方法中的临时变量,也不是方法参数,它不可能作为根,在内部类
    中也没有变量引用它,它的根在内部类外部的那个方法中,如果这时外面变量重指向其它对象,则这个对象就失去了引用,
    可能被回收,而由于内部类回调方法大多数在其它线程中执行,可能还要在回收后还会继续访问它.这将是什么结果?

    而使用final修饰符不仅会保持对象不会改变,而且编译器还会持续维护这个对象在回调方法中的生命周期.所以这才是final
    变量和final参数的根本意义.

  85. yeml[未注册用户]
    *.*.*.*
    链接

    yeml[未注册用户] 2009-04-20 12:53:00

    老赵,我自己利用业余时间开发了一个商机发布软件,请问你有没有类似的经验?就是自己做了产品,然后怎么卖怎么合作的问题

  86. yeml[未注册用户]
    *.*.*.*
    链接

    yeml[未注册用户] 2009-04-20 12:56:00

    功能性能特性跟商务快车什么的不差多少,我以后打算主要做这个产品,不打工了,不过现在没什么积蓄,要打工养家

  87. yeml[未注册用户]
    *.*.*.*
    链接

    yeml[未注册用户] 2009-04-20 13:47:00

    想了想,这个问题的确是个好问题
    。net跟java的不同之处在于。net会自己把这个变量的引用提升到全局根,而java则要求显示的设置为final
    这个就是典型的微软风格了

    自动设置为全局根后,后面的赋值会替换前面的,而多线程调用的时间没办法把握的。

    而java设置为final后,一个提升成为了全局根,二个是防止了重复赋值。

    至于我之前讲的GC的问题,我还没有尝试过,不知道是不是这样,有可能我理解有误


    内部类是java的必考点来的

  88. 老赵
    admin
    链接

    老赵 2009-04-20 13:57:00

    @yeml
    我没做过这个东西

  89. 老赵
    admin
    链接

    老赵 2009-04-20 13:58:00

    @yeml
    java其实是要求一种强制约束,C#是把这个决定交给程序员了,不过会让不了解情况的程序员误用。
    如果真要多线程设置,那么务必需要自己进行控制,方法也很多,怎么能说“没有办法把握”呢?所以我很不喜欢java这种限制。

  90. yeml[未注册用户]
    *.*.*.*
    链接

    yeml[未注册用户] 2009-04-20 14:09:00

    嗯,你说的是对的。
    cnblogs为啥不搞搞广州的聚会呢。。。。。这个问题我提过好多次了

    我没说是这个东西,我的意思是这样的一种经验。
    c#的多线程的确是很强大的
    以前java都没有信号量这个玩意

  91. yeml[未注册用户]
    *.*.*.*
    链接

    yeml[未注册用户] 2009-04-20 14:15:00

    能跟人讨论技术问题,能学到东西,真是一个开心的事情


  92. 老赵
    admin
    链接

    老赵 2009-04-20 16:21:00

    @yeml
    这篇文章其实没有讲清东西,它说“而使用final修饰符不仅会保持对象不会改变,而且编译器还会持续维护这个对象在回调方法中的生命周期.所以这才是final
    变量和final参数的根本意义. ”,但是没有谈到final是怎么“维护生命周期”的。
    我怀疑java也和C#差不多,文章里也没有GC是针对线程的。

  93. yeml[未注册用户]
    *.*.*.*
    链接

    yeml[未注册用户] 2009-06-08 14:18:00

    哈哈哈哈,看了29楼的办法之后,想起听过的一个故事

    那个舒肤佳做香皂的,但是经常会出现香皂出场了,但是只有一个盒子,里面的香皂忘装了的情况

    然后请了很多博士来设置方案,比如精妙的重力感应等

    但是后来一个流水线工人给出了一个非常简单的办法:在流水线旁边放个风扇吹,如果是空盒子就会被吹出来

    29的办法跟zhao的办法比起来就有这么个意思

    不过原理倒都是同一个原理

  94. 老赵
    admin
    链接

    老赵 2009-06-08 18:24:00

    @yeml
    原理其实……不知道算不算一样。
    不过博士生的研究是有价值的,我的方法没啥价值。

  95. Peter.X.Gao
    *.*.*.*
    链接

    Peter.X.Gao 2009-09-07 00:51:00

    老赵,我自己试了一下,您和overred的方法好像都不行。

    我修改成下面,成功了。请教是怎么回事?

    if (batchItems.Count > 1000)
    {
    List<Item> tempItems = batchItems.FindAll(o => true);
    ThreadPool.QueueUserWorkItem((o) =>
    {
    DataContext db = new DataContext();
    db.Items.InsertAllOnSubmit(tempItems);
    db.SubmitChanges();
    });
    batchItems = new List<Item>();
    }

  96. 老赵
    admin
    链接

    老赵 2009-09-07 00:56:00

    @Peter.X.Gao
    你的方法和overred的方法没有任何区别,FindAll是多余的,白白构造了一个新的容器而已。

  97. Peter.X.Gao
    *.*.*.*
    链接

    Peter.X.Gao 2009-09-07 01:33:00

    @Jeffrey Zhao

    我找到问题出在哪儿了,开始我用overred的方法老报异常是因为我最后一句写的是:batchItems.Clear()

    谢谢老赵

  98. 畅想自由
    *.*.*.*
    链接

    畅想自由 2010-01-30 14:11:00

    我一直比较迷惑,当线程被唤醒后,他是如何知道,要调用哪个tempItems 的? 这个时候已经有一堆tempItems的指针引用,如何对应呢? 是编译器做了手脚?

  99. 老赵
    admin
    链接

    老赵 2010-01-30 14:27:00

    @畅想自由
    那就去看看编译器的结果,尝试验证你的猜想。

  100. hunk
    110.210.126.*
    链接

    hunk 2011-09-01 00:07:47

    up up up

  101. badnewfish
    113.132.212.*
    链接

    badnewfish 2013-07-06 14:40:44

  102. badnewfish
    113.132.212.*
    链接

    badnewfish 2013-07-06 16:38:46

    还有个问题 如下代码

    System.Runtime.Remoting.Messaging.CallContext.LogicalSetData("Name", "GaoTest");
    ThreadPool.QueueUserWorkItem((state) =>
    {
        MessageBox.Show("1Name=" + state);
    }, CallContext.LogicalGetData("Name"));
    
    ///阻止执行上下文流动
    ExecutionContext.SuppressFlow();
    
    
    ThreadPool.QueueUserWorkItem((state) =>
    {
        MessageBox.Show("2Name=" + state);
    }, CallContext.LogicalGetData("Name"));
    ///恢复执行上下文流动
    ExecutionContext.RestoreFlow();
    

    按理说应该能够阻止上下文的流动,但是实际不行,如果改成下面的方式则可以:

    System.Runtime.Remoting.Messaging.CallContext.LogicalSetData("Name", "GaoTest");
    ThreadPool.QueueUserWorkItem((state) =>
    {
        MessageBox.Show("1Name=" +  CallContext.LogicalGetData("Name"));
    });
    
    ///阻止执行上下文流动
    ExecutionContext.SuppressFlow();
    
    
    ThreadPool.QueueUserWorkItem((state) =>
    {
        MessageBox.Show("2Name=" + CallContext.LogicalGetData("Name"));
    });
    ///恢复执行上下文流动
    ExecutionContext.RestoreFlow();
    

    请问这是否和匿名委托共享了局部变量state的原因造成的呢?

  103. godzza
    127.0.0.*
    链接

    godzza 2013-10-20 19:19:45

    不知道老赵的blog是不是出了点问题?25楼的评论看不到了 如果继续采用闭包则可以这样解决,但不知道这种陷阱的会觉得增加submitItems很莫名其妙

    static void Process()
    { 
        List<Item> batchItems = new List<Item>();
        foreach (var item in ...)
        {
            batchItems.Add(item);
    
            if (batchItems.Count > 1000)
            {
                var submitItems = new List<Item>(batchItems);
                ThreadPool.QueueUserWorkItem((o) =>
                {
                    DataContext db = new DataContext();
                    db.Items.InsertAllOnSubmit(submitItems);
                    db.SubmitChanges();
                });                    
    
                batchItems = new List<Item>();
            }
        }
    }
    

发表回复

登录 / 登录并记住我 ,登陆后便可删除或修改已发表的评论 (请注意保留评论内容)

昵称:(必填)

邮箱:(必填,仅用于Gavatar

主页:(可选)

评论内容(大于5个字符):

  1. Your Name yyyy-MM-dd HH:mm:ss

使用Live Messenger联系我