C#中的易失性字段

时间:2011-02-25 03:41:05

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

来自规范10.5.3易失性字段:


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

  • 参考类型。

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

  • 具有枚举基类型的枚举类型 of byte,sbyte,short,ushort,int, 或者uint。


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

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

5 个答案:

答案 0 :(得分:1)

基本上,使用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 Lippert中的the comments section所指出的那样。事实上,这里的最新版本意味着.NET运行时将尝试(此处不做任何保证),以防止只要认为可能,就会在读取操作之间发生对易失性成员的写入。这将有助于不同的线程读取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关键字的用法,实际工作方式以及使用它的可能后果。

答案 1 :(得分:1)

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

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

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

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

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

答案 2 :(得分:0)

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

答案 3 :(得分:0)

这是一个有根据的猜测答案...如果我错了,请不要把我击倒太多!

documentation for volatile州:

  

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

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

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

此外,只有一些结构可能是易失性的 - 大小为4字节的结构。确定大小的代码 - 结构定义 - 可以在程序的完全独立的部分中,也可以将字段定义为volatile。这可能会令人困惑,因为更新结构定义会产生意想不到的后果。

因此,虽然技术上可以允许某些结构变得不稳定,但正确使用的注意事项将足够复杂,以至于缺点会超过其益处。

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

答案 4 :(得分:0)

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

亲吻 - 保持简单西蒙 - 这将使规范更复杂,并且难以实现此功能。所有语言功能都从minus 100 points开始,是否增加了一小部分struts volatile真正值得101分的能力?

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