【问题标题】:What is the reason for Task.IsCompleted to use cached flags?Task.IsCompleted 使用缓存标志的原因是什么?
【发布时间】:2020-05-28 17:41:14
【问题描述】:

我正在查看 System.Threading.Tasks(.NET 标准 2.0)中 Task 的一些实现细节,我发现了这段有趣的代码:

internal volatile int m_stateFlags;

...

public bool IsCompleted
{
  get
  {
    int stateFlags = m_stateFlags; // enable inlining of IsCompletedMethod by "cast"ing away the volatiliy
    return IsCompletedMethod(stateFlags);
  }
}

// Similar to IsCompleted property, but allows for the use of a cached flags value
// rather than reading the volatile m_stateFlags field.
private static bool IsCompletedMethod(int flags)
{
  return (flags & TASK_STATE_COMPLETED_MASK) != 0;
}

通过阅读 C# 参考指南,我了解到 volatile 是为了防止编译器/运行时/硬件优化导致对字段的读/写重新排序。为什么在这种特定情况下将字段指定为 volatile 只是为了在通过将字段分配给变量来读取字段时忽略波动性?

这似乎是有意的,但我不清楚意图背后的原因。 另外,“抛弃”波动性是一种常见的做法吗?在哪些情况下我想这样做,而我绝对想避免这样做?

非常感谢任何有助于我更清楚地理解这段代码的信息。

谢谢,

【问题讨论】:

  • 这是非常顽皮的代码,肯定是在 .NET 4.0 的 Volatile.Read() 稳定之前编写的。这是 Thread.VolatileRead() 的错误修复,它错误地占用了完整的内存屏障。抖动在具有弱内存模型(arm、itanium)的处理器上为 volatile 关键字提供额外语义,提供读取获取语义。在这个属性 getter 中没有意义,但在其他两个地方使用它。顺便说一句,OptionsMethod() 也是如此。除了高成本之外,它还避免了多次测试旗帜时的比赛。一致性是一件好事。

标签: c# .net task task-parallel-library volatile


【解决方案1】:

volatilea syntax sugar of Volatile.Read and Volatile.Write。它确保任何 CPU 将同时看到相同的数据。

所以它只对 volatile 变量进行一次读取。那么这可能会在属性,方法中多次使用(失去易失性)。所以它看起来像是对变量进行快照并对其值进行多次计算。

internal volatile int m_stateFlags;

public bool IsCompleted
{
  get
  {
      int stateFlags = m_stateFlags;
      return IsCompletedMethod(stateFlags);
  }
}

【讨论】:

  • 如果有引用类型,你需要小心使用这种技术
【解决方案2】:

所以,前段时间 JIT 不知道如何内联方法,如果它们的任何实际参数是可变的。但后来它在 PR 中被修复:Allow inlining with volatile actual argument exprs #7332

出于性能原因需要m_stateFlags 的本地副本:方法调用非常昂贵,JIT 无法内联该调用。

所以,现在您可以创建一个 PR 并删除该冗余副本,因为它是 still there 和修复 is there

【讨论】:

    【解决方案3】:

    更多来自.NET Framework Reference Source的cmets:

    “抛弃”波动性以启用 OptionsMethod 的内联。

    读取 volatile m_stateFlags 字段一次,并将其缓存以供后续操作使用。

    获取状态标志的缓存副本。这应该有助于我们在执行此方法期间更改标志时获得一致的视图。

    正如您已经知道的那样,它显然用于三件事:

    1. 启用某些方法的内联(显然访问易失性字段块内联?)
    2. 减少 volatile 读取调用的数量(可能只是出于第三个原因而已)
    3. 确保在执行此方法期间该值未更改(例如,来自不同的线程)

    现在回答你的问题。

    为什么在这种特定情况下将字段指定为 volatile 只是为了在通过将字段分配给变量来读取字段时忽略波动性?

    Task.m_stateFlags 显然是为了尽快从任何线程更新。 IsCompletedMethod 从多个地方调用(例如Start(TaskScheduler scheduler)),您不希望m_stateFlags 在方法执行过程中更改值。 IsCompleted 看起来只是 IsCompletedMethod 的光滑“包装”。

    “抛弃”波动性是一种常见的做法吗?在哪些情况下我想这样做与我绝对想避免它的情况?

    我没有很多经验,也没有看到很多用法,但基于@StepUp 和Task 用法给出的link - 如果你愿意,你可以让你的字段不稳定使其在处理器和线程之间保持同步。

    这听起来与Interlockedlock() 用例非常相似,不是吗?

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2017-04-10
      • 1970-01-01
      • 2023-03-05
      • 2019-05-31
      • 2021-10-27
      • 1970-01-01
      • 1970-01-01
      • 2014-12-29
      相关资源
      最近更新 更多