C# 中的易失性字段

发布于 2024-10-19 07:01:02 字数 576 浏览 5 评论 0原文

根据规范 10.5.3 易失性字段:


易失性字段的类型必须是以下类型之一:

  • 引用类型。

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

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


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

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

From the specification 10.5.3 Volatile fields:


The type of a volatile field must be one of the following:

  • A reference-type.

  • The type byte, sbyte, short, ushort,
    int, uint, char, float, bool,
    System.IntPtr, or System.UIntPtr.

  • An enum-type having an enum base type
    of byte, sbyte, short, ushort, int,
    or uint.


First I want to confirm my understanding is correct: I guess the above types can be volatile because they are stored as a 4-bytes unit in memory(for reference types because of its address), which guarantees the read/write operation is atomic. A double/long/etc type can't be volatile because they are not atomic reading/writing since they are more than 4 bytes in memory. Is my understanding correct?

And the second, if the first guess is correct, why a user defined struct with only one int field in it(or something similar, 4 bytes is ok) can't be volatile? Theoretically it's atomic right? Or it's not allowed simply because that all user defined structs(which is possibly more than 4 bytes) are not allowed to volatile by design?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(5

心头的小情儿 2024-10-26 07:01:02

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

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

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

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

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

So, I suppose you propose the following point to be added:

  • A value type consisting only of one field which can be legally marked volatile.

First, fields are usually private, so in external code, nothing should depend on a presence of a certain field. Even though the compiler has no issue accessing private fields, it is not such a good idea to restrict a certain feature based on something the programmer has no proper means to affect or inspect.

Since a field is usually a part of the internal implementation of a type, it can be changed at any time in a referenced assembly, but this could make a piece of C# code that used the type illegal.

This theoretical and practical reason means that the only feasible way would be to introduce a volatile modifier for value types that would ensure that point specified above holds. However, since the only group of types that would benefit from this modifier are value types with a single field, this feature probably wasn't very high on the list.

晨曦慕雪 2024-10-26 07:01:02

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

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

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

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

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

对于多线程,以下用法是正确的,因为每个线程都会替换 MyVolatileMember 指针。在这里,易失性确保其他线程将看到存储在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正如 Eric Lippert 所指出的,这里迟到价值的概念有点误导在评论部分。事实上,这里的最新意味着 .NET 运行时将尝试(此处不保证)只要它认为可能,就可以防止在读取操作之间发生对易失性成员的写入。这将有助于不同的线程读取易失性成员的新值,因为它们的读取操作可能会在对该成员的写入操作之后进行排序。但这里还有更多值得信赖的概率。

2一般来说,易失性可以用在任何不可变对象上,因为修改总是意味着用不同的值重新分配字段。以下代码也是使用 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);

我建议您查看 本文。它彻底解释了 volatile 关键字的用法、它的实际工作方式以及使用它可能产生的后果。

Basically, usage of the volatile keyword can sometimes be misleading. Its purpose is to allow that the latest value (or actually, an eventually fresh enough value)1 of the respective member is returned when accessed by any thread.

In fact, this is true to value types only2. Reference type members are represented in memory as the pointers to a location in the heap where the object is actually stored. So, when used on a reference type, volatile ensures you only get the fresh value of the reference (the pointer) to the object, not the object itself.

If you have a volatile List<String> myVolatileList which is modified by multiple threads by having elements added or removed, and if you expect it to be safely accessing the latest modification of the list, you are actually wrong. In fact, you are prone to the same issues as if the volatile keyword was not there -- race conditions and/or having the object instance corrupted -- it does not assist you in this case, neither it provides you with any thread safety.

If, however, the list itself is not modified by the different threads, but rather, each thread would only assign a different instance to the field (meaning the list is behaving like an immutable object), then you are fine. Here is an example:

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

The following usage is correct with respect to multi-threading, as each thread would replace the MyVolatileMember pointer. Here, volatile ensures that the other threads will see the latest list instance stored in the MyVolatileMember field.

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();

In contrast, the below code is error prone, because it directly modifies the list. If this code is executed simultaneously with multiple threads, the list may become corrupted, or behave in an inconsistent manner.

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

Let us return to value types for a while. In .NET all value types are actually reassigned when they are modified, they are safe to be used with the volatile keyword - see the code:

public class HasVolatileValueType
{
    public volatile int MyVolatileMember;
}

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

1The notion of lates value here is a little misleading, as noted by Eric Lippert in the comments section. In fact latest here means that the .NET runtime will attempt (no guarantees here) to prevent writes to volatile members to happen in-between read operations whenever it deems it is possible. This would contribute to different threads reading a fresh value of the volatile member, as their read operations would probably be ordered after a write operation to the member. But there is more to count on probability here.

2In general, volatile is OK to be used on any immutable object, since modifications always imply reassignment of the field with a different value. The following code is also a correct example of the use of the volatile keyword:

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);

I'd recommend you to take a look at this article. It thoroughly explains the usage of the volatile keyword, the way it actually works and the possible consequences to using it.

野の 2024-10-26 07:01:02

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

I think it is because a struct is a value type, which is not one of the types listed in the specs. It is interesting to note that reference types can be a volatile field. So it can be accomplished with a user-defined class. This may disprove your theory that the above types are volatile because they can be stored in 4 bytes (or maybe not).

如果没结果 2024-10-26 07:01:02

这是对答案的有根据的猜测...如果我错了,请不要太严厉地批评我!

易失性文档指出:

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

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

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

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

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

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

This is an educated guess at the answer... please don't shoot me down too much if I am wrong!

The documentation for volatile states:

The volatile modifier is usually used for a field that is accessed by multiple threads without using the lock statement to serialize access.

This implies that part of the design intent for volatile fields is to implement lock-free multithreaded access.

A member of a struct can be updated independently of the other members. So in order to write the new struct value where only part of it has been changed, the old value must be read. Writing is therefore not guaranteed to require a single memory operation. This means that in order to update the struct reliably in a multithreaded environment, some kind of locking or other thread synchronization is required. Updating multiple members from several threads without synchronization could soon lead to counter-intuitive, if not technically corrupt, results: to make a struct volatile would be to mark a non-atomic object as atomically updateable.

Additionally, only some structs could be volatile - those of size 4 bytes. The code that determines the size - the struct definition - could be in a completely separate part of the program to that which defines a field as volatile. This could be confusing as there would be unintended consequences of updating the definition of a struct.

So, whereas it would be technically possible to allow some structs to be volatile, the caveats for correct usage would be sufficiently complex that the disadvantages would outweigh the benefits.

My recommendation for a workaround would be to store your 4-byte struct as a 4-byte base type and implement static conversion methods to use each time you want to use the field.

情场扛把子 2024-10-26 07:01:02

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

KISS - 保持简单 Simon - 它会使规范更加复杂,并且很难实现此功能。所有语言功能均以负 100 分开始,添加具有少数struts volatile的能力真的值101分吗?

兼容性 - 抛开序列化问题 - 通常向类型 [class, struct] 添加新字段是一种安全的向后源兼容举措。如果添加字段不应该破坏任何人的编译。如果添加字段时结构的行为发生变化,则会破坏这一点。

To address the second part of your question, I would support the language designers decision based on two points:

KISS - Keep It Simple Simon - It would make the spec more complex and implementations hard to have this feature. All language features start at minus 100 points, is adding the ability to have a small minority of struts volatile really worth 101 points?

Compatibility - questions of serialization aside - Usually adding a new field to a type [class, struct] is a safe backwards source compatible move. If you adding a field should not break anyones compile. If the behavior of structs changed when adding a field this would break this.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文