Hello World
Spiga

您能看出这个Double Check里的问题吗?(解答)

2009-09-02 17:16 by 老赵, 8409 visits

问题请参考:您能看出这个Double Check里的问题吗?

已经很有很多朋友得到了结果,是由于m_categories过早初始化,而导致double check的验证条件被破坏(或者说,满足)。

private object m_mutex = new object();
private Dictionary<int, Category> m_categories;

public Category GetCategory(int id)
{
    if (this.m_categories == null)
    {
        lock (this.m_mutex)
        {
            if (this.m_categories == null)
            {
                LoadCategories();
            }
        }
    }

    return this.m_categories[id];
}

private void LoadCategories()
{
    this.m_categories = new Dictionary<int,Category>();
    this.Fill(GetCategoryRoots());
}

private void Fill(IEnumerable<Category> categories)
{
    foreach (var cat in categories)
    {
        this.m_categories.Add(cat.CategoryID, cat);
        Fill(cat.Children);
    }
}

假设第一个线程进入了GetCategory方法,它自然可以畅通无阻地执行LoadCategories。只可惜,在LoadCategories方法的第一行就为m_categories设置了一个空字典。如果现在立即有另一个线程访问了GetCategory方法,就会发现m_categories字段不是null,并直接执行this.m_categories[id]这行代码——但此时,第一个线程还没有将这个字典填充完毕!

因此,这段代码其实是一个有问题的Double Check实现。那么我们该怎么改呢?

一位匿名朋友提出,可以增加一个标记,用来表示有没有初始化完毕。如下:

private bool m_initialized = false;

public Category GetCategory(int id)
{
    if (!this.m_initialized)
    {
        lock (this.m_mutex)
        {
            if (!this.m_initialized)
            {
                LoadCategories();
                this.m_initialized = true;
            }
        }
    }

    return this.m_categories[id];
}

这是个非常漂亮的做法,完全没有问题。不过我并没有使用这种修改方式。

private void LoadCategories()
{
    var categories = new Dictionary<int,Category>();

    Fill(categories, GetCategoryRoots());

    this.m_categories = categories;
}

private static void Fill(Dictionary<int, Category> container, IEnumerable<Category> categories)
{
    foreach (var cat in categories)
    {
        container.Add(cat.CategoryID, cat);
        Fill(container, cat.Children);
    }
}

我稍稍改变了一下Fill方法,它不再直接访问m_categories字段,而是把内容填充至container参数中。而在LoadCategories方法中,我们创建一个字典,但是直到填充完毕后才将其赋给m_categories字段。这样就保证了在m_categories字段不为null的时候,一定已经初始化完毕了。这也是一种可行的办法。我没有使用第一种做法的原因,并不是因为所谓的“节省空间”,而是……一下子就想到了第二种做法。:)

这里反映了Double Check在使用时的一个准则:在满足if条件的时候,一定要确保所有的初始化已经完成了。或者说,一定要将“满足if条件”的操作放在初始化完毕之后进行。至于是否使用某个标记,倒不是什么大问题。

如果您使用.NET编写代码,目前已经没有问题了,但是在某些情况下这样的代码还是会出现问题。我认为这也是多线程编程时最麻烦的地方——就是所谓的“Memory Consistency Model”。

为了性能考虑,编译器在将文本代码转化为机器码,以及CPU在执行机器码时都会对执行进行“重新排序(reorder)”,reorder的作用是为了提升性能。虽然从单线程的角度来看,reorder不会形成问题,但是在多线程的环境中,reorder就会破坏代码的逻辑了。如果没有一个“标准”在进行统一的话,不同的编译器,虚拟机,CPU架构都会有不同的reorder策略。例如微软并行库之父Joe Duffy这篇文章中简单地提到了不同平台(JVM / CLR 2.0)或不同CPU架构(x86 / IA64)下reorder规则的区分。

而臭名昭著的Double Check的bug便是由于store reordering造成的。在JVM或普通的C、C++中并不保证store reordering不会发生。也就是说,您在代码中看到的两个变量的“设置”顺序,并不代表CPU在执行的时候,也是同样的效果。因此,如果你观察下面的代码:

class Foo { 
    private Helper helper = null;
    public Helper getHelper() {
        if (helper == null) 
            synchronized(this) {
                if (helper == null) 
                    helper = new Helper();
            }
        return helper;
    }
}

看上去这是一段再正常不过的实现Double Check的Java代码,但是由于发生了store reordering,可能在Helper构造函数中的操作还没有全部执行完成之前,就设置了helper字段。因此另一个线程就可能会访问到一个没有初始化完整的Helper对象。如果您对这个话题感兴趣,可以参考《The "Double-Checked Locking is Broken" Declaration》。

而在CLR 2.0中,只会发生load reordering,而不会出现store reordering。于是.NET中编写的Double Check代码不会出现任何问题。那么CLR是如何保证在不同的CPU平台上出现相同的行为呢?那是因为CLR会根据不同的平台,在合适的情况下插入一些辅助代码(如Memory Barrier),可见CLR为我们的并行编程环境已经形成了一个相对比较方便的平台了——虽然,并行编程还是很困难。

(似乎关于Memory Model的有些说法不太确切,随时更新,希望了解这些的朋友们也可以提点意见,我晚上回家后再查些资料)

Creative Commons License

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

Add your comment

63 条回复

  1. 麒麟.NET
    *.*.*.*
    链接

    麒麟.NET 2009-09-02 17:17:00

    第一个阅读,第一个留言,第一个寂寞

  2. 麒麟.NET
    *.*.*.*
    链接

    麒麟.NET 2009-09-02 17:18:00

    晕,原来下午这么热闹,错过了,遗憾啊

  3. DiggingDeeply
    *.*.*.*
    链接

    DiggingDeeply 2009-09-02 17:24:00

    这里的reorder是指CPU的乱序执行吗?

  4. 老赵
    admin
    链接

    老赵 2009-09-02 17:26:00

    @DiggingDeeply
    对的。

  5. RednaxelaFX
    *.*.*.*
    链接

    RednaxelaFX 2009-09-02 17:31:00

    JVM在Java 5修改了内存模型后也会插入memory barrier了……

  6. 老赵
    admin
    链接

    老赵 2009-09-02 17:34:00

    @RednaxelaFX
    这倒愿意改,语言倒不肯改。

  7. 兴百放
    *.*.*.*
    链接

    兴百放 2009-09-02 17:56:00

    如果A,B两个线程同时到达 if(this.m_categories == null){之后,而在lock(this.m_mutex)之前,会有什么事情发生呢?

  8. Anders Liu
    *.*.*.*
    链接

    Anders Liu 2009-09-02 18:07:00

    其实,在多线程环境中,遇到这种集合操作,最好都是用一个临时变量进行操作,等结果出来后再赋值给字段。

  9. 老赵
    admin
    链接

    老赵 2009-09-02 18:09:00

    @Anders Liu
    其实除了这种情况,我还真想不起来哪有类似的操作。
    其他关于集合的操作往往就上读写锁了,也就double check会有这需求。

  10. 老赵
    admin
    链接

    老赵 2009-09-02 18:10:00

    @兴百放
    没啥,两个线程继续下去。

  11. OwnWaterloo
    *.*.*.*
    链接

    OwnWaterloo 2009-09-02 18:24:00

    Jeffrey Zhao:
    而在CLR 2.0中,只会发生load reordering,而不会出现store reordering。于是.NET中编写的Double Check代码不会出现任何问题。



    Jeffrey Zhao:
    那么CLR是如何保证在不同的CPU平台上出现相同的行为呢?那是因为CLR会根据不同的平台在合适的情况下插入一些辅助代码(如Memory Barrier),可见CLR为我们的并行编程环境已经形成了一个相对比较方便的平台了



    是否可以理解为:
    在需要插入barrier代码的平台上, 所谓的double check —— 即在首次初始化完毕后、只需进行普通if检测 —— 仅仅是表象~~~

  12. 请输入您的昵称![未注册用户]
    *.*.*.*
    链接

    请输入您的昵称![未注册用户] 2009-09-02 18:26:00

    private object m_mutex = new object();是线程相关的,你lock谁?
    static在哪里?没有看见。。。

  13. 老赵
    admin
    链接

    老赵 2009-09-02 18:28:00

    @OwnWaterloo
    我没有听懂你的意思,我的意思是:在某些平台上如果没有插入barrier,一些“赋值”的顺序可能会改变。
    例如,应该是这样赋值的:
    Helper.aaa = 1;
    helper = {Helper对象的地址}

    某些平台如果交换一下,就出问题了。

  14. 老赵
    admin
    链接

    老赵 2009-09-02 18:30:00

    @请输入您的昵称!
    上篇文章该说明的都已经说清了。
    为什么一谈到lock,不少朋友就认为一定要static呢?
    实例对象的成员也可以同时被n个线程访问,不是吗?

  15. 请输入您的昵称![未注册用户]
    *.*.*.*
    链接

    请输入您的昵称![未注册用户] 2009-09-02 18:37:00

    刚刚看到原来的帖子,嘎嘎。。。

    实例对象的方法可以被n个线程访问呢,没问题。但是方法的参数、每个field,是存放在每个线程自己的stack上地。。。何来冲突只有?

    之所以要lock staic的,因为在整个class的势力范围内,它是唯一的,它才是所有人都可以访问的。文中的m_categories是属于每个线程的。

  16. 老赵
    admin
    链接

    老赵 2009-09-02 18:41:00

    @请输入您的昵称!
    m_categories是分配在堆(heap)里的对象,不属于任何线程,不在任何线程的堆栈上,是共享的。

  17. OwnWaterloo
    *.*.*.*
    链接

    OwnWaterloo 2009-09-02 18:49:00

    @Jeffrey Zhao

    Jeffrey Zhao:
    上篇文章该说明的都已经说清了。
    为什么一谈到lock,不少朋友就认为一定要static呢?
    实例对象的成员也可以同时被n个线程访问,不是吗?


    因为被singleton洗脑的家伙很多~~~ 不必在意。



    我的意思是, 在需要barrier的平台上, 第1个if :
    if (!initialized)
    就已经不是普通的检测了。
    double check 的高效,就只是形式上的?
    虽然代码中表现出的只有if 判断, 但实际上运行的代码做的工作已经不止if 这么简单?



    并且,任意时刻,store reordering都是被禁止的吗?
    编译器应该不会聪明到"看出这是一个double check,所以禁止这个函数中的store reordering", 而是对所有函数, 都禁止?
    即使不会被多线程访问?

  18. 请输入您的昵称![未注册用户]
    *.*.*.*
    链接

    请输入您的昵称![未注册用户] 2009-09-02 18:53:00

    LZ,设想一下,你的第一个线程对m_categories进行了clear,然后加入了1、2、3,同时第二个线程也对m_categories进行了clear,然后加入了4、5、6。

    你认为这两个线程中的m_categories的内容,会有冲突吗?可能会变成1、2、5或者2、3、6之类的?

    当然不会。因为m_categories是属于CategoryLoader类中的实例。我们实际应用,可能是:

    thread1中:
    CategoryLoader cl1 = new CategoryLoader();

    thread1中:
    CategoryLoader cl2 = new CategoryLoader();

    上面的cl1/cl2只是为了方便,实际上当然都是写作cl而已。

    此时,这两个线程中都new了一个对象,里面“各自”包含了一个m_categories。so,这里面,这是两个m_categories对象,而非一个。。。

  19. 老赵
    admin
    链接

    老赵 2009-09-02 18:57:00

    @请输入您的昵称!
    你这么用当然不会有问题,但是我不是说了“同一个对象也可以被多个线程同时访问”吗?
    上一篇文章也说了“多个线程同时访问GetCategory方法要保证正确”。
    这就是“单个方法”的线程安全,例如我之前写过,ASP.NET MVC全局共享一个ControllerFactory对象,但是它有个bug导致不是线程安全的。
    单个对象的实例方法被多个线程共享,这种例子数不胜数,.NET 4.0中新增大量Concurrent集合,都是为了处理并发情况的。
    看到一个实例对象,就以为它不是共享的,这种时代已经过去了阿,接下来并行会愈演愈烈的。

  20. 老赵
    admin
    链接

    老赵 2009-09-02 19:04:00

    @OwnWaterloo
    在运行时,平台不会去关注是不是一个double check,但是它会改变一些“赋值”的顺序。
    例如,我们代码是这样写的:

    if (!init)
    {
        lock(mutex)
        {
            if (!init)
            {
                Load();
                init = true;
            }
        }
    }
    但是经过reorder之后,在实际执行时的“效果”就是:
    if (!init)
    {
        lock(mutex)
        {
            if (!init)
            {
                init = true;
                Load();
            }
        }
    }
    
    根据CLR 2.0的标准,任意时刻,store reordering都是禁止的。

  21. 请输入您的昵称![未注册用户]
    *.*.*.*
    链接

    请输入您的昵称![未注册用户] 2009-09-02 19:06:00

    这段没看懂,“同一个对象也可以被多个线程同时访问”,能否给个例子?callee和caller的双方的代码。

    多谢!

  22. OwnWaterloo
    *.*.*.*
    链接

    OwnWaterloo 2009-09-02 19:06:00

    @请输入您的昵称!

    你这么理解吧, 这个方法:
    public Category GetCategory(int id);
    使用了缓式计算。


    CategoryLoader cl = new CategoryLoader();
    然后将cl丢给若干个线程。
    这些线程会有可能同时调用GetCategory, 也有可能没有任何一个线程会调用GetCategory。

    那么, 使用了缓式计算的GetCategory, 需要保护吗?

  23. 请输入您的昵称!
    *.*.*.*
    链接

    请输入您的昵称! 2009-09-02 19:14:00

    up一下!

    LZ最好写一个命令行的多县城的例子,能演示出这种不安全性。简单的WriteLine什么abc就行了。

  24. 老赵
    admin
    链接

    老赵 2009-09-02 19:16:00

    @请输入您的昵称!
    CategoryLoader cl = new CategoryLoader();
    ThreadPool.QueueUserWorkItem((o) => cl.GetCategory(1));
    ThreadPool.QueueUserWorkItem((o) => cl.GetCategory(2));
    ThreadPool.QueueUserWorkItem((o) => cl.GetCategory(3));

  25. 请输入您的昵称!
    *.*.*.*
    链接

    请输入您的昵称! 2009-09-02 19:42:00

    嗯,受教了。

    还是要细究一下,threadpool里面三个工作线程,那么传入的cl.

  26. ZelluX[未注册用户]
    *.*.*.*
    链接

    ZelluX[未注册用户] 2009-09-02 19:52:00

    @DiggingDeeply
    编译器也会做

  27. ZelluX[未注册用户]
    *.*.*.*
    链接

    ZelluX[未注册用户] 2009-09-02 19:54:00

    CLR的memory model禁止任何reordering吗?这样貌似挺影响效率的

  28. 老赵
    admin
    链接

    老赵 2009-09-02 19:58:00

    @ZelluX
    load不限制,限制的是store。

  29. Anders Liu
    *.*.*.*
    链接

    Anders Liu 2009-09-02 22:23:00

    Jeffrey Zhao:
    @Anders Liu
    其实除了这种情况,我还真想不起来哪有类似的操作。
    其他关于集合的操作往往就上读写锁了,也就double check会有这需求。



    我借宝地再举个例子吧,就不单开贴扯淡了。和这种情况比较远,但有异曲同工的感觉。

    protected void OnSomeEvent(EventArgs e)
    {
    if(this.SomeEvent != null)
    this.SomeEvent(this, e);
    }

    这是最常见的触发事件的方法,但在多线程环境下会出现问题。如线程1执行到if语句发现this.SomeEvent不为null,而恰好此时线程2使用-=运算符移除了事件处理器,导致this.SomeEvent为null。接下来线程1就会出现问题。

    解决的方法出奇简单,就是使用局部变量暂存下类型成员的值:

    protected void OnSomeEvent(EventArgs e)
    {
    var handler = this.SomeEvent;

    if(handler != null)
    handler(this, e);
    }

    别反驳我,有意见找Anders去,那个发明C#的Anders,这是他说的。

  30. dongzz
    *.*.*.*
    链接

    dongzz 2009-09-02 22:38:00

    老赵是好人啊.
    Anders Liu这样一说,看来我以前写的很多代码都是有bug的.

  31. 未登录[未注册用户]
    *.*.*.*
    链接

    未登录[未注册用户] 2009-09-02 23:05:00

    if (this.m_categories == null)
    {
    lock (this.m_mutex)
    {
    if (this.m_categories == null)
    {
    LoadCategories();
    }
    }
    }
    第一个this.m_categories == null的判断去掉似乎也可以,就是每次都要lock一下,资源上是浪费的。
    于是乎,大致测试了下,3000个线程并发下,7s多比lz的方法5s多慢近2s。(加标识m_initialized的方法和lz的方法差不多,都是5s多)

  32. 老赵
    admin
    链接

    老赵 2009-09-02 23:07:00

    @Anders Liu
    你说的这个我肯定同意的,hoho,不过真没看出和集合有啥关系。
    我也顺便补充一个:在默认情况下,事件的+=以及-=操作是线程安全的。
    只是,如果你显式地自己写add和remove就不一定了,恩恩……

  33. 老赵
    admin
    链接

    老赵 2009-09-02 23:16:00

    未登录:
    第一个this.m_categories == null的判断去掉似乎也可以,就是每次都要lock一下,资源上是浪费的。


    这是一定的,所以要“double check”,既线程安全,又保证性能。

  34. flyingchen
    *.*.*.*
    链接

    flyingchen 2009-09-02 23:38:00

    取消前一篇的提问,因为你这里已经有答案了

  35. 老赵
    admin
    链接

    老赵 2009-09-02 23:44:00

    @flyingchen
    恶……迟了……

  36. flyingchen
    *.*.*.*
    链接

    flyingchen 2009-09-02 23:54:00

    @Jeffrey Zhao
    有收获。之前我还特意这样写....5555555

  37. 夏草
    *.*.*.*
    链接

    夏草 2009-09-03 09:05:00

    @Jeffrey Zhao
    赵老师,你的右边的Follow me怎么打不开?我想去看看

  38. 老赵
    admin
    链接

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

    @夏草
    我不知道,我不知道,我不知道……嗷嗷

  39. 鹤冲天
    *.*.*.*
    链接

    鹤冲天 2009-09-03 12:36:00

    哈哈,我的想法是正确的。解决办法也是用一个局部变量,填充完后,再赋给m_categories。
    多线程编程确实很复杂,稍有不慎就会出现bug,多线程编程一定要仔仔细细的考虑啊。

  40. CoolCode
    *.*.*.*
    链接

    CoolCode 2009-09-03 13:35:00

    看来我有些代码要改了...

  41. CoolCode
    *.*.*.*
    链接

    CoolCode 2009-09-03 13:42:00

    不知道我的代码在多线程下有没有问题,代码是这样的:

    private static readonly Dictionary<string, IServiceProvider> _providerCache = new Dictionary<string, IServiceProvider>();
    
    //在Load方法中:
                        if (!_providerCache.ContainsKey(key))
                        {
                            lock (insertMutex)
                            {
                                if (!_providerCache.ContainsKey(key))//double check
                                {
                                    _providerCache.Add(key, p);
                                }
                            }
                        }
    ....
    

  42. 老赵
    admin
    链接

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

    @CoolCode
    有问题。
    _providerCache.ContainsKey不是线程安全的方法。
    对于集合的这种的操作往往要使用读写锁。
    http://www.cnblogs.com/JeffreyZhao/archive/2009/09/01/get-action-name-from-expression-tree-by-ActionNameAttribute.html

  43. CoolCode
    *.*.*.*
    链接

    CoolCode 2009-09-03 13:50:00

    谢谢,看来还是得改。

  44. OwnWaterloo
    *.*.*.*
    链接

    OwnWaterloo 2009-09-03 16:15:00

    Jeffrey Zhao:
    在运行时,平台不会去关注是不是一个double check,但是它会改变一些“赋值”的顺序。



    这我了解, 而且据我所知, 存在某些平台存在运行时的code reordering。

    源代码级:
    由程序员保证算法正确: 肯定是读、检测、读、检测、写的顺序。

    机器码级:
    对于C/C++: 如果输出的汇编代码显示出编译器生成的目标代码并不是这样的顺序、而是被编译器重排、并且会影响算法正确性; 那么,如果需要, 可以降低该翻译单元的优化等级、 或者直接写汇编代码。

    对.net, 还有一个中间代码的编译过程。
    也就是说, 光凭中间代码是不能判断最终执行的顺序是否满足需要。
    但老赵你也说了, clr会保证不会发生交迭写, 也就不需要再去看最终机器码了。


    但, 即使机器码上顺序是正确的,依然能保证cpu是按机器码的顺序来执行。 在执行指令时, 某些cpu依然会再次执行code reordering。


    要在这个级别上继续保证正确, 就只能插入barrier指令。
    同样,如你所说, .net也帮我们做了。


    那么, 我的疑问(本来还有一个, 仔细想想,是自己想错了,插入barrier不会造成第1个if的低效), 也是这位同学的疑问:

    ZelluX:CLR的memory model禁止任何reordering吗?这样貌似挺影响效率的



    可惜老赵你理解错了。


    也许是我问得不好,这次问严格一点:
    “.net 禁止任何函数中的STORE reordering吗? ”
    既然cpu要reordering肯定是有它的理由的——不然不会无故去制造这种麻烦。
    但如果对所有函数都禁止reordering, 会不会影响效率?
    有没有机制可以打开某些函数的reordering?

  45. 老赵
    admin
    链接

    老赵 2009-09-03 16:19:00

    @OwnWaterloo
    .net禁止任何函数中的store reordering,这是标准中写清楚的。
    至于能否打开,我也不知道,但是既然标准写着了,应该没有提供这样的方式吧。

  46. OwnWaterloo
    *.*.*.*
    链接

    OwnWaterloo 2009-09-03 16:28:00

    @Jeffrey Zhao

    那么这句话:

    Jeffrey Zhao:
    可见CLR为我们的并行编程环境已经形成了一个相对比较方便的平台了



    这么说也没错咯?
    可见CLR —— 以可能会降低所有函数的效率为代价 —— 为我们的并行编程环境已经形成了一个相对比较方便的平台了。

  47. 老赵
    admin
    链接

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

    @OwnWaterloo
    这以这么说吧,不过现在看来性能也不成为问题,所以我马后炮地认为这个决策也是蛮正确的。

  48. OwnWaterloo
    *.*.*.*
    链接

    OwnWaterloo 2009-09-03 16:41:00

    @Jeffrey Zhao
    嗯, 绝大多数情况下, 还是稍微牺牲点运行时间, 提供一个比较爽的编程环境, 节省大量开发时间比较划算~

  49. ColorSmart[未注册用户]
    *.*.*.*
    链接

    ColorSmart[未注册用户] 2009-09-06 11:31:00

    @Jeffrey Zhao
    这种情况下应该是没有问题的吧,在写Dict的时候是lock了的,而且他的读也不是赋值,只是判断。

  50. ColorSmart[未注册用户]
    *.*.*.*
    链接

    ColorSmart[未注册用户] 2009-09-06 11:33:00

    Jeffrey Zhao:
    @CoolCode
    有问题。
    _providerCache.ContainsKey不是线程安全的方法。
    对于集合的这种的操作往往要使用读写锁。
    http://www.cnblogs.com/JeffreyZhao/archive/2009/09/01/get-action-name-from-expression-tree-by-ActionNameAttribute.html


    这种情况下应该是没有问题的吧,在写Dict的时候是lock了的,而且他的读也不是赋值,只是判断。

  51. ColorSmart[未注册用户]
    *.*.*.*
    链接

    ColorSmart[未注册用户] 2009-09-06 11:50:00

    @Jeffrey Zhao
    再问老赵一个问题,现在的double check是不是都可以改为用Lazy Init?
    比如这个GetCategory(int id) 可以这样写:
    public Category GetCategory(int id)
    {
    Lazy<Category> lazyGetCategory = new Lazy<Category> (()=>{LoadCategories();return m_Category[id];});



    return lazyGetCategory.Value;
    }


  52. 老赵
    admin
    链接

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

    @ColorSmart
    是的,事实上我大部分情况下用的就是Lazy。

  53. 老赵
    admin
    链接

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

    ColorSmart:
    这种情况下应该是没有问题的吧,在写Dict的时候是lock了的,而且他的读也不是赋值,只是判断。


    有问题的,你看代码,可能会出现一个线程正在Write,一个线程正在Read,这对于字典是线程不安全的。
    用rwlock的目的,就是避免在Write的时候产生其他write或read。

  54. ColorS[未注册用户]
    *.*.*.*
    链接

    ColorS[未注册用户] 2009-09-07 13:39:00

    Jeffrey Zhao:

    ColorSmart:
    这种情况下应该是没有问题的吧,在写Dict的时候是lock了的,而且他的读也不是赋值,只是判断。


    有问题的,你看代码,可能会出现一个线程正在Write,一个线程正在Read,这对于字典是线程不安全的。
    用rwlock的目的,就是避免在Write的时候产生其他write或read。


    确实是线程不安全,但是这里的code应该没有什么risk issue。
    假设有两个Thread:T1,T2。线程不安全的情况:
    T1读到dict的时候T2正在写:两种情况:
    1.这时候如果T1读出来是null,T1进lock的时候T2已经写完退出,没有问题。
    2.T1读到key,return。

    根本原因就是内层的lock可以看作一个rwlock

  55. 老赵
    admin
    链接

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

    @ColorS
    但是字典不是像你一样说的那样,读取和修改都是原子性的啊。
    如果一个线程在写字典,另一个线程同时在读,就可能会出现问题,例如直接抛出异常了。
    因为字典的修改操作不是原子性的,里面设计到很多步骤(如改变字段,创建新数组,复制等等)。
    所以,必须避免出现两个线程同时写,或者一个写的时候同时另一个再读的情况发生。

  56. grey0502
    166.111.68.*
    链接

    grey0502 2010-09-05 04:23:39

    那调用CPU提供的阻止换序的一条指令呢? 比如在powerpc下:

    #define barrier() _asm_ volatile ("lwsync")
    
    ...
    
    private void LoadCategories()
    {
        this.m_categories = new Dictionary<int,Category>();
        barrier();
        this.Fill(GetCategoryRoots());
    }
    
  57. 老赵
    admin
    链接

    老赵 2010-09-05 18:05:44

    @grey0502

    这就不是托管平台下的做法了……

  58. 不明白
    116.228.220.*
    链接

    不明白 2011-01-29 12:23:44

    看了你的文章,我还是不明白,在什么情况下会发生这和情况;”多线程同时访问一个对像“

    CategoryLoader cl = new CategoryLoader();
    cl.GetCategory(1); //这样调用有问题吗?
    

    http的每一次请求,就只有产生一个线程;为什么会出现”多线程同时访问一个对像“呢?请解答??

  59. 老赵
    admin
    链接

    老赵 2011-01-30 17:51:37

    @不明白

    关HTTP什么事情,没人说这段代码是跑在ASP.NET里的。

  60. yangtou
    115.172.88.*
    链接

    yangtou 2011-09-06 23:33:00

    private bool m_initialized = false;
    
    public Category GetCategory(int id)
    {
        if (!this.m_initialized)
        {
            lock (this.m_mutex)
            {
                if (!this.m_initialized)
                {
                    LoadCategories();
                    this.m_initialized = true;
                }
            }
        }
    
        return this.m_categories[id];
    }
    

    这个不对吧,可能load reorder,先读到m_categories(null),后读到m_initialized(true)

  61. 老赵
    admin
    链接

    老赵 2011-09-07 00:09:14

    @yangtou

    在C#和.NET里不会的。

  62. yangtou
    211.161.218.*
    链接

    yangtou 2011-09-12 21:13:19

    我认为C#编译器完全可以将load mcategories reorder到load minitialized之前,这样不会改变单线程下程序本来的语义. 相当于branch prediction,因为多数情况下m_initialized == true。

    ld mcategories ;mcategories = null

    ;另一个线程 st m_initialized = true.

    ld minitialized ;minitialized = true

    return m_categories; null

  63. 我是路过的
    112.91.65.*
    链接

    我是路过的 2011-10-28 02:13:19

    话说这种写法很危险,换个语言,环境就要杯具。 还是加个bool来判断靠谱。

发表回复

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

昵称:(必填)

邮箱:(必填,仅用于Gavatar

主页:(可选)

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

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

使用Live Messenger联系我