代码之家  ›  专栏  ›  技术社区  ›  Jeff Cyr

方法内联优化会导致竞争条件吗?

  •  15
  • Jeff Cyr  · 技术社区  · 15 年前

    如本问题所示: Raising C# events with an extension method - is it bad?

    我正在考虑使用此扩展方法安全地引发事件:

    public static void SafeRaise(this EventHandler handler, object sender, EventArgs e)
    {
        if (handler != null)
            handler(sender, e);
    }
    

    但迈克·罗森布鲁姆在乔恩·斯基特的回答中提出了这个问题:

    你们需要加上 [MethodImpl(MethodImplOptions.NoInlining)] 这些扩展方法的属性 或者你试图复制 委托给临时变量可以 通过抖动进行优化, 允许空引用 例外。

    我在发布模式下做了一些测试,看看当扩展方法没有标记为noinlining时,我是否可以获得竞争条件:

    int n;
    EventHandler myListener = (sender, e) => { n = 1; };
    EventHandler myEvent = null;
    
    Thread t1 = new Thread(() =>
    {
        while (true)
        {
            //This could cause a NullReferenceException
            //In fact it will only cause an exception in:
            //    debug x86, debug x64 and release x86
            //why doesn't it throw in release x64?
            //if (myEvent != null)
            //    myEvent(null, EventArgs.Empty);
    
            myEvent.SafeRaise(null, EventArgs.Empty);
        }
    });
    
    Thread t2 = new Thread(() =>
    {
        while (true)
        {
            myEvent += myListener;
            myEvent -= myListener;
        }
    });
    
    t1.Start();
    t2.Start();
    

    我在发布模式下运行了一段时间测试,并且从未有过NullReferenceException。

    那么,迈克·罗森布卢姆在他的评论和方法中是错误的,内联不能导致种族状况吗?

    事实上,我想真正的问题是,是否会说:

    while (true)
    {
        EventHandler handler = myEvent;
        if (handler != null)
            handler(null, EventArgs.Empty);
    }
    

    while (true)
    {
        if (myEvent != null)
            myEvent(null, EventArgs.Empty);
    }
    
    3 回复  |  直到 14 年前
        1
  •  7
  •   Jon Skeet    15 年前

    问题不在于该方法是内联的——不管它是否是内联的,它都可能是通过内存访问执行有趣的事情时的抖动。

    但是,我不相信 首先是一个问题。几年前,人们把它作为一个问题提出,但我相信这被认为是对记忆模型的错误解读。变量只有一个逻辑“读取”,而抖动不能优化这一点,使值在副本的一次读取和副本的第二次读取之间发生变化。

    编辑:为了澄清,我完全理解为什么这会给你带来问题。基本上有两个线程修改同一个变量(因为它们使用捕获的变量)。代码完全有可能这样发生:

    Thread 1                      Thread 2
    
                                  myEvent += myListener;
    
    if (myEvent != null) // No, it's not null here...
    
                                  myEvent -= myListener; // Now it's null!
    
    myEvent(null, EventArgs.Empty); // Bang!
    

    在这段代码中,这比正常情况下要稍微不明显,因为变量是一个捕获变量,而不是一个正常的静态/实例字段。但同样的原则也适用。

    安全提升方法的要点是将引用存储在一个局部变量中,该变量 不能 从任何其他线程修改:

    EventHandler handler = myEvent;
    if (handler != null)
    {
        handler(null, EventArgs.Empty);
    }
    

    现在线程2是否更改 myEvent -它无法更改处理程序的值,因此您将无法 NullReferenceException .

    如果JIT 内联的 SafeRaise ,它将被内联到此代码段-因为内联参数最终有效地成为一个新的局部变量。只有当JIT 不正确地 通过保持两个独立的 我的事件 .

    现在,关于你为什么 只有 在调试模式下看到了这种情况:我怀疑在附加了调试程序的情况下,线程之间的中断空间要大得多。可能还发生了一些其他的优化——但没有。 介绍 有破损的,没关系。

        2
  •  5
  •   Daniel    14 年前

    这是内存模型问题。

    基本上,问题是:如果我的代码只包含一个逻辑读取,优化器是否可以引入另一个读取?

    令人惊讶的是,答案是: 也许吧

    在clr规范中,没有什么可以阻止优化器这样做。优化不会破坏单线程语义,并且只保证为易失性字段保留内存访问模式(即使这是一个不是100%正确的简单应用程序)。

    因此,无论使用局部变量还是参数,代码都是 非线程安全 .

    但是,Microsoft.NET Framework记录了不同的内存模型。在该模型中,优化器不允许引入读取,并且 你的密码是安全的 (独立于内联优化)。

    也就是说,使用[MethodImplOptions]似乎是一种奇怪的黑客行为,因为阻止优化器引入读取只是不内联的副作用。我会用易挥发的字段或线程来代替。

        3
  •  1
  •   Vlad    15 年前

    使用正确的代码,优化不应该改变其语义。因此,如果代码中没有错误,优化器就不能引入任何错误。

    推荐文章