有人能给出一个关于C#中
volatile
关键字的好解释吗?它解决了哪些问题,又没有解决哪些问题?在哪些情况下使用它可以避免使用锁定?volatile
关键字的好解释吗?它解决了哪些问题,又没有解决哪些问题?在哪些情况下使用它可以避免使用锁定?volatile
关键字如何有效地发挥其预期的作用。假设我们想要实现一个简单版本的Task<TResult>
类。我们的类只包含两个字段:_completed
和_result
,并且只支持两个操作:设置_result
和仅在_completed
为真时读取它。让我们来看一个无锁实现这个简单类型的例子:public class MyTask<TResult>
{
private volatile bool _completed;
private TResult _result;
public void UnsafeSetResult(TResult result)
{
_result = result;
_completed = true;
}
public bool TryGetResult(out TResult result)
{
if (_completed)
{
result = _result;
return true;
}
result = default;
return false;
}
}
UnsafeSetResult
方法被称为 "不安全",因为它在整个 MyTask<TResult>
实例的生命周期中只能被调用一次。我们将在本答案的最后解决这个限制,但现在让我们假设通过我们应用程序的结构可以简单地强制执行这个规则。例如,我们可以有一个单独的专用线程负责创建 MyTask<TResult>
对象,并在它们上调用 UnsafeSetResult
。TryGetResult
能够正常工作?是什么阻止了调用TryGetResult
的线程接收到一个tornTResult
值?答案在于_completed
字段被声明为volatile
,以及_completed
和_results
字段的赋值和读取顺序。我们希望确保在将_result
的值完全存储在字段中之前,没有线程会尝试读取它。请注意,我们对TResult
泛型参数没有任何限制,因此它完全可以是一个大型结构,比如一个decimal
、一个Int128
、一个ValueTuple<long,long,long,long>
等等。如果我们不小心,一个线程可能会读取一个半写入的_result
值,其中一半的字节仍然未初始化。这被称为“tearing”,这是我们想要防止的灾难。_completed
设置为true
来确保不会发生撕裂,这是在我们分配_result
之后,并在确认_completed
为true
之后读取_result
。在_completed
字段上使用volatile
关键字可以确保C#编译器和.NET Jitter不会发出访问/修改计算机内存的CPU指令以不同的顺序。如果你不知道的话,C#编译器、.NET Jitter和CPU处理器都可以重新排序程序的指令,只要这种重新排序不会影响在单个线程上运行时程序的行为。volatile
对UnsafeSetResult
方法产生的确切影响。_result = result;
不能被移动到 _completed = true;
之后。volatile
对 TryGetResult
方法有什么影响:result = _result;
不能被移动到 if (_completed)
之前。public class MyTask<TResult>
{
private volatile int _state; // 0:incomplete, 1:reserved, 2:completed
private TResult _result;
public bool TrySetResult(TResult result)
{
if (Interlocked.CompareExchange(ref _state, 1, 0) == 0)
{
_result = result;
_state = 2;
return true;
}
return false;
}
public bool TryGetResult(out TResult result)
{
if (_state == 2)
{
result = _result;
return true;
}
result = default;
return false;
}
}
Task<TResult>
类有一个类似的 internal volatile int m_stateFlags;
字段(源代码),在给 internal TResult? m_result;
字段赋值之前,其中的一个位会被原子地翻转(CompletionReserved
,源代码)。volatile
分别为加载和存储操作提供了获取/释放语义。请参考链接:https://preshing.com/20120913/acquire-and-release-semantics/. - undefinedvolatile
应用于字段A,以保护非volatile
字段B。我几乎完全使用了C#开发人员可用的术语和官方文档,以免让读者感到陌生。 :-) - undefinedvolatile int v;
x = 5;
print(y);
...
v = 3; // previous read/write operations on this thread cannot be moved after this (even if there are no dependencies)
...
print(v); // following read/write operations on this thread cannot be moved before this (even if there are no dependencies)
x = 5;
print(y);
...
// thread 1
v1 = 1
...
v2 = 2
// thread 1
volatile int v1 = 0;
v1 = 1;
print(v2)
// thread 2
volatile int v2 = 0;
v2 = 1;
print(v1);
0
0
打印自从读取可以在写入之前移动。
规范也没有提到缓存。但是文档中在一个注释中提到了:
易失性读取和写入确保值被读取或写入内存,而不是缓存在处理器寄存器中。
如果这是真的,意味着你可以像这样在线程之间使用它们进行同步:
// main thread
v = false // at t = 0s
// worker thread 1
...
// value is written directly to memory
v = true // at t = 1s
...
// worker thread 2
...
// latest value from memory is retrieved
if (v)
{
print("this is guaranteed to be printed after t = 1s")
}
...
这似乎与之前的注释相矛盾,即您不能假设您正在获取最新的值。
在日常编程中,我认为最好避免使用volatile
,只在其特定的弱要求能够提供比锁等更强的同步机制更好的性能优化的情况下小心使用它。
volatile
解决了哪些问题,又解决不了哪些问题?”而你的回答基本上是“我不知道volatile
解决了什么问题,所以我不鼓励你使用它,除非你需要性能并且小心使用。”这并没有帮助。我们已经有Eric Lippert通过Ohad Schneider的回答来劝阻人们使用volatile
了。我们不需要更多的恐慌和消极情绪。人们应该基于知识和可靠信息做出决策,而不是在困惑中。 - Theodor Zouliasvolatile
,就像这里(CancellationTokenSource
的_state
)一样,直到他们修复文档中的不一致之处?否则,如果允许微软使用它,为什么其他人不能使用呢? - Theodor Zouliaslock
并不会给你比使用volatile
更及时的值。无论如何,系统的自然延迟都会对你施加影响。 - Theodor Zoulias多个线程可以访问变量。 变量将被更新为最新值。