繁体   English   中英

在结构方法中将“this”存储在局部变量中有什么好处?

[英]What advantage is there to storing “this” in a local variable in a struct method?

我今天浏览了.NET Core源代码树,并在System.Collections.Immutable.ImmutableArray<T>遇到了这种模式

T IList<T>.this[int index]
{
    get
    {
        var self = this;
        self.ThrowInvalidOperationIfNotInitialized();
        return self[index];
    }
    set { throw new NotSupportedException(); }
}

这种模式(存储this在一个局部变量)似乎在此文件中一致地应用每当this本来就在同样的方法多次引用,而不是在它只能引用一次。 所以我开始思考这样做的相对优势是什么; 在我看来,优势可能与性能有关,所以我走了这条路线更远......也许我忽略了别的东西。

CIL说时发出的“存储this在当地的”模式似乎看起来像一个ldarg.0 ,然后ldobj UnderlyingType ,然后stloc.0以至于后来引用来自ldloc.0而不是裸ldarg.0像这将是多次使用this

也许ldarg.0明显慢于ldloc.0 ,但对于C#-to-CIL转换或JITter来说找不到为我们优化这个的机会都不够,这样写这个奇怪的外观更有意义C#代码中的模式,否则我们会在struct实例方法中发出两个ldarg.0指令?

更新:或者,你知道,我可以查看该文件顶部的注释,它可以准确地解释发生了什么......

正如您已经注意到的,System.Collections.Immutable.ImmutableArray <T>是一个结构

public partial struct ImmutableArray<T> : ...
{
    ...

    T IList<T>.this[int index]
    {
        get
        {
            var self = this;
            self.ThrowInvalidOperationIfNotInitialized();
            return self[index];
        }
        set { throw new NotSupportedException(); }
    }

    ...

var self = this; 创建结构的副本, 这个简称。 为什么要这样做呢? 这个结构源代码注释解释了为什么有必要:

///这种类型应该是线程安全的。 作为结构,它不能保护自己的领域
///在其成员在其他线程上执行时从一个线程更改
///因为结构可以简单地通过重新分配含有领域的地方修改
///这个结构。 因此,非常重要的是
/// **每个成员都应该只取消引用此ONCE。 **
///如果一个成员需要引用数组字段,那就算是对它的解引用了。
///调用其他实例成员(属性或方法)也算作取消引用它。
///任何需要多次使用它的成员必须改为
///将其分配给局部变量,并将其用于代码的其余部分。
///这有效地将结构中的一个字段复制到局部变量中
///它与其他线程绝缘。

简而言之,如果有可能其他线程正在对结构的字段进行更改或更改结构(例如,通过重新分配此结构类型的类成员字段),而get方法正在执行,因此可以导致不良副作用,然后get方法必须首先在处理结构之前制作结构的(本地)副本。

更新:还请阅读supercats的答案 ,它详细解释了必须满足哪些条件,以便像构建结构的本地副本(即var self = this; )这样的操作是线程安全的,如果这些条件会发生什么不满足。

如果底层存储位置是可变的,则.NET中的结构实例总是可变的,并且如果底层存储位置是不可变的,则始终是不可变的。 结构类型可能“假装”是不可变的,但.NET将允许结构类型实例被任何可以编写它们所在的存储位置的东西修改,结构类型本身在这个问题上没有发言权。

因此,如果有一个结构:

struct foo {
  String x;
  override String ToString() {
    String result = x;
    System.Threading.Thread.Sleep(2000);
    return result & "+" & x;
  }
  foo(String xx) { x = xx; }
}

一个是在具有foo[]类型的相同数组myFoos的两个线程上调用以下方法:

myFoos[0] = new foo(DateTime.Now.ToString());
var st = myFoos[0].ToString();

完全可能的是,首先启动的线程将使其ToString()值报告其构造函数调用所写的时间和其他线程的构造函数调用所报告的时间,而不是两次报告相同的字符串。 对于其目的是验证结构字段然后使用它的方法,在验证和使用之间进行字段更改将导致该方法使用未经验证的字段。 复制结构字段的内容(通过仅复制字段,或通过复制整个结构)避免了这种危险。

请注意,对于包含Int64UInt64Double类型的字段或包含多个字段的结构,可能会出现类似var temp=this;的语句var temp=this; 而另一个线程被重写其中位置发生在一个线程this已经存储,最终可能会复制其保持的新旧内容的任意的混合物的结构。 仅当结构包含引用类型的单个字段或32位或更小的基元的单个字段时,才能保证与写入同时发生的读取将产生结构实际保持的某个值,甚至可能有一些怪癖(例如至少在VB.NET中,像someField = New foo("george")这样的语句可能会在调用构造函数之前清除someField )。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM