【问题标题】:Volatile fields in C#C# 中的可变字段
【发布时间】:2011-02-25 03:41:04
【问题描述】:

来自规范 10.5.3 Volatile fields:


volatile 字段的类型必须是以下之一:

  • 引用类型。

  • 类型字节、sbyte、short、ushort、 int, uint, char, float, bool, System.IntPtr 或 System.UIntPtr。

  • 具有枚举基类型的枚举类型 字节,sbyte,short,ushort,int, 或单位。


首先我想确认我的理解是正确的:我猜上述类型可能是易失的,因为它们在内存中存储为一个 4 字节的单元(对于引用类型,因为它的地址),这保证了读/写操作是原子的。 double/long/etc 类型不能是 volatile,因为它们不是原子读/写,因为它们在内存中超过 4 个字节。我的理解正确吗?

第二个,如果第一个猜测是正确的,为什么用户定义的结构中只有一个int 字段(或类似的东西,4 字节就可以)不能是易失性的? 理论上它是原子的,对吗?或者仅仅因为所有用户定义的结构(可能超过 4 个字节)都不允许 volatile设计

【问题讨论】:

  • stackoverflow.com/questions/4727068/…(阅读 Eric Lippert 对他的回答的评论)
  • @The Scrum Meister:感谢您提供的链接,它完美地回答了我的第一个问题。但是第二个呢?为什么 C# 中 4 字节的用户定义结构不能是 volatile 的?
  • @Danny 可能是因为他们想让它变得简单。请记住 Typically, the common language runtime controls the physical layout of the data fields of a class or structure in managed memory. (msdn.microsoft.com/en-us/library/…),因此除非您使用 StructLayoutAttribute,否则您无法确定结构的布局。
  • @xanatos:我也认为他们想让事情变得简单。但不太确定,所以我希望有人能提供一个规范的证据,以防我在接下来的重要采访中被问到这个问题。
  • @Danny A IntPtr 是 native (platform-specific) size integer stackoverflow.com/questions/1148177/…

标签: c# .net clr language-features volatile


【解决方案1】:

所以,我想你建议添加以下几点:

  • 一种仅由一个字段组成的值类型,可以合法地标记为 volatile。

首先,字段通常是私有的,因此在外部代码中,任何东西都不应该依赖于某个字段的存在。即使编译器在访问私有字段时没有问题,但基于程序员没有适当的方法来影响或检查的东西来限制某个功能并不是一个好主意。

由于字段通常是类型内部实现的一部分,因此可以在引用的程序集中随时更改它,但这可能会使使用该类型的一段 C# 代码非法。

这个理论和实践原因意味着唯一可行的方法是为值类型引入volatile 修饰符,以确保上面指定的点成立。但是,由于唯一可以从该修饰符中受益的类型是具有单个字段的值类型,因此该功能可能在列表中的排名不是很高。

【讨论】:

  • +1 非常合理。这是我刚接触该语言时提出的一个老问题。我现在完全同意你的看法。
【解决方案2】:

基本上,volatile 关键字的使用有时会产生误导。其目的是允许在被任何线程访问时返回相应成员的最新值(或者实际上是最终足够新鲜的值)1

事实上,这仅对值类型2是正确的。引用类型成员在内存中表示为指向堆中实际存储对象的位置的指针。因此,当用于引用类型时,volatile 可确保您仅获得对象的引用(指针)的新值,而不是对象本身。

如果您有一个volatile List<String> myVolatileList,它由多个线程通过添加或删除元素修改,并且如果您希望它可以安全地访问列表的最新修改,那么您实际上是 错。实际上,您很容易遇到与 volatile 关键字不存在相同的问题——竞争条件和/或对象实例损坏——在这种情况下它对您没有帮助,也没有为您提供任何线程安全。

但是,如果列表本身没有被不同的线程修改,而是每个线程只会为该字段分配一个不同的实例(这意味着列表的行为就像一个不可变对象) ,那你就好了。这是一个例子:

public class HasVolatileReferenceType
{
    public volatile List<int> MyVolatileMember;
}

对于多线程,以下用法是正确的,因为每个线程都将替换MyVolatileMember 指针。在这里,volatile 确保其他线程将看到存储在 MyVolatileMember 字段中的最新列表实例。

HasVolatileReferenceTypeexample = new HasVolatileReferenceType();
// instead of modifying `example.MyVolatileMember`
// we are replacing it with a new list. This is OK with volatile.
example.MyVolatileMember = example.MyVolatileMember
     .Where(x => x > 42).ToList();

相比之下,下面的代码容易出错,因为它直接修改了列表。如果此代码与多个线程同时执行,则列表可能会损坏或行为不一致。

example.MyVolatileMember.RemoveAll(x => x <= 42);

让我们暂时回到值类型。在 .NET 中,所有值类型在被修改时实际上都会重新分配,它们可以安全地与 volatile 关键字一起使用 - 参见代码:

public class HasVolatileValueType
{
    public volatile int MyVolatileMember;
}

// usage
HasVolatileValueType example = new HasVolatileValueType();
example.MyVolatileMember = 42;

1这里的lates value 的概念有点误导,正如Eric Lippertthe comments section 中所指出的那样。实际上,这里的最新意味着 .NET 运行时将尝试(此处不保证)以防止在读取操作之间发生写入 volatile 成员,只要它认为可能。这将有助于不同的线程读取 volatile 成员的新值,因为它们的读取操作可能会在对该成员的写入操作之后进行排序。但这里还有更多要依靠概率。

2一般来说,volatile 可以用于任何 不可变 对象,因为修改总是意味着用不同的值重新分配字段。以下代码也是volatile关键字使用的正确示例:

public class HasVolatileImmutableType
{
    public volatile string MyVolatileMember; 
}

// usage
HasVolatileImmutableType example = new HasVolatileImmutableType();
example.MyVolatileMember = "immutable";
// string is reference type, but is *immutable*, 
// so we need to reasign the modification result it in order 
// to work with the new value later
example.MyVolatileMember = example.MyVolatileMember.SubString(2);

我建议你看看this article。它彻底解释了 volatile 关键字的用法、它的实际工作方式以及使用它的可能后果。

【讨论】:

  • 这是非常不安全的。另一个线程可以在读取(用于子字符串)和写入之间改变 volatile 成员。
  • 我知道我迟到了很多年来批评这个答案,但我注意到它多次提出同样的误导性声明。 Volatile 不保证您阅读的是最新值,因为存在一致的、普遍同意的“最新值”之类的概念是完全错误的.很多人相信你所做的:volatile 给了你“新鲜”的保证。不幸的是,微软文档中的许多地方都强化了这种错误的信念系统。 C# 规范说明了 volatile 的真正作用。
  • 简而言之: volatile 限制了编译器、运行时和 CPU 重新排序某些读写相对于彼此的能力。读可以及时后移,写可以及时前移; volatile 限制了合法移动的集合。 这就是它保证做的所有事情,你不应该依赖它做更多的事情。特别是,C# 规范明确指出不能保证所有线程都同意一致的读取和写入顺序
  • 既然不能保证顺序一致,也就不存在“最新值”这回事,因为有“最新值”就意味着大家都同意“最新”的含义。
  • @EricLippert,确实如此。我已经更正了我的主张,因为确实没有保证正在读取的数据的绝对新鲜度。感谢您指出这一点,这是一个重要的话题,不要让人们感到困惑。
【解决方案3】:

我认为这是因为结构是一种值类型,它不是规范中列出的类型之一。有趣的是,引用类型可以是一个 volatile 字段。所以它可以通过用户定义的类来完成。这可能会反驳您的理论,即上述类型是易失性的,因为它们可以存储在 4 个字节中(或者可能不是)。

【讨论】:

  • 错了。引用是 4 个字节。
  • 易变的是 4 字节的引用,而不是实际使用的类型。引用本身与值类型没有太大区别。
【解决方案4】:

这是对答案的有根据的猜测......如果我错了,请不要对我失望!

documentation for volatile 声明:

volatile修饰符通常用于多线程访问而不使用lock语句序列化访问的字段。

这意味着 volatile 字段的部分设计意图是实现无锁多线程访问。

结构的成员可以独立于其他成员进行更新。因此,为了写入新的结构值,其中只有部分已更改,必须读取旧值。因此,不能保证写入需要单个内存操作。这意味着为了在多线程环境中可靠地更新结构,需要某种锁定或其他线程同步。在不同步的情况下从多个线程更新多个成员可能很快会导致违反直觉的结果(如果不是技术上的损坏):使 struct volatile 会将非原子对象标记为可原子更新。

此外,只有一些结构可能是易失的 - 大小为 4 字节的结构。确定大小的代码 - 结构定义 - 可以与将字段定义为 volatile 的程序完全分开。这可能会造成混淆,因为更新结构的定义会产生意想不到的后果。

因此,虽然允许某些结构易失性在技术上是可行的,但正确使用的注意事项会非常复杂,以至于弊大于利。

我对解决方法的建议是将您的 4 字节结构存储为 4 字节基本类型,并实现静态转换方法以在您每次要使用该字段时使用。

【讨论】:

    【解决方案5】:

    为了解决您问题的第二部分,我会基于两点支持语言设计者的决定:

    KISS - 保持简单 Simon - 这会使规范变得更加复杂,并且难以实现此功能。所有语言功能都从minus 100 points 开始,添加让少数struts volatile 的能力真的值101 分吗?

    兼容性 - 除了序列化问题 - 通常将新字段添加到类型 [class, struct] 是一种安全的向后源兼容移动。如果你添加一个字段不应该破坏任何人的编译。如果在添加字段时结构的行为发生了变化,这将破坏 this。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2021-12-08
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多