65.9K
CodeProject 正在变化。 阅读更多。
Home

易失性, 如其应有

starIconstarIconstarIconstarIconstarIcon

5.00/5 (6投票s)

2013 年 4 月 22 日

CPOL

5分钟阅读

viewsIcon

20992

downloadIcon

216

本文介绍了一个类,该类允许以预期的方式进行易失性读写。

引言

这是一篇高级文章。

在许多情况下,您根本不需要使用 volatile 关键字或 Thread.Volatile* 方法,因为所有同步原语都会为您完成工作,并且通常更易于使用。

但是,如果您确实关心性能并想使用 volatile 字段,您应该明白

  1. 如果您将字段标记为 volatile,则所有对它的读写都将是 volatile 的,即使您在锁内访问它
  2. 如果您决定不将字段标记为 volatile,然后使用 Thread.VolatileRead()Thread.VolatileWrite(),您将执行完整的内存栅栏,而实际上只需要半个内存栅栏

注意:在 .NET 4.5 提供了同样按预期工作的 Volatile 类之前,我写了这篇文章。但我仍在将 .NET 4.0 用于我的项目,因此这个类对我来说很有用,并且我认为即使您不需要它,它也是一个有趣的话题。

理解缺失的信息

也许第 1 点并没有完全讲清楚。我做了测试来确保这一点。volatile 关键字使所有读写都变成 volatile,但它们使用半个内存栅栏(即:仅读 [acquire] 栅栏和仅写 [release] 栅栏),而 Thread.VolatileRead()Thread.VolatileWrite() 始终使用完整的内存栅栏。很长一段时间以来,我以为这是 .NET 本身的一个 bug。我真的以为 volatile 修饰符会在 IL 中将字段标记为 volatile,并且所有读写都是常规的 IL 读写,只是作用于一个 volatile 字段。

但事实并非如此。实际上,问题在于 C#(它不允许我们指定哪个操作是 volatile 的,使得 volatile 成为一个全有或全无的修饰符)以及 Thread.Volatile* 的实现。在 IL 级别,我们可以使用 volatile 修饰符来前缀 ldfldstfld(例如)。而这种 volatile 修饰符只会应用正确的半个内存栅栏,而不是完整的内存栅栏。

我必须说,我是偶然发现这个的。我当时在查看 IL 指令,只是出于其他原因,当我看到 volatile 前缀时。所以我决定尝试一下……我真的很想在 C# 中使用这种正确的行为,所以我决定做一些测试。

第一次测试 - 毫无用处

在我的第一次测试中,我编写了一个使用 volatile 前缀的 DynamicMethod。然后我生成了一个委托,如下所示:

public int ReadDelegate(ref int variable);

然后我进行了测试。事实上,我生成了一个非 volatile 的委托和一个 volatile 的委托,并且通过性能差异,我认为它运行良好。然而,使用委托的虚拟调用使得半个内存栅栏的代码比使用 Thread.VolatileRead() 方法的完整内存栅栏更慢,所以我决定放弃这个想法。

第二次测试 - IL + C#

.NET 的一个优点是我们可以用一种语言编写库,然后用另一种语言访问它。当然,为单个类生成整个库会更好,但既然这可以解决问题,我决定尝试一下。但是,由于我从未只用 IL 创建过库,所以我决定用 C# 创建一个新的类库,其中包含一个类和一个方法,编译它,然后使用 ildasm 获取该库的 IL。

我最初的代码如下所示:

public static class Volatile
{
  public static int Read(ref int variable)
  {
    return variable;
  }
}

当我反编译它时,我得到了这段代码:

.class public abstract auto ansi sealed beforefieldinit Pfz.Volatile
       extends [mscorlib]System.Object
{
  .method public hidebysig static int32  Read(int32& variable) cil managed
  {
    // Code size       3 (0x3)
    .maxstack  8
    IL_0000:  ldarg.0
    IL_0001:  ldind.i4
    IL_0002:  ret
  }
}

事实上,我使用 ildasm 转储了整个库的代码,但我决定不把整个代码放在文章中,因为它太长了。

然后,我更改了 IL 代码:

.class public abstract auto ansi sealed beforefieldinit Pfz.Volatile
       extends [mscorlib]System.Object
{
  .method public hidebysig static int32  Read(int32& variable) cil managed
  {
    // Code size       3 (0x3)
    .maxstack  1
    IL_0000:  ldarg.0
    volatile.
    IL_0001:  ldind.i4
    IL_0002:  ret
  }
}

最后,我使用 ilasm 编译器,并附带 /DLL 参数编译了这段代码。所以,我在一个测试应用程序中使用了这个库,并且性能与使用 volatile 关键字进行读取的性能基本相同。但是现在,我有了一个选项,可以使用非易失性变量进行常规读取,或者进行半个内存栅栏的读取。这正是我想要的。

所以,最后一步是创建所有 VolatileRead()VolatileWrite() 重载。为此,我查看了 Thread.VolatileRead() 方法的所有重载,以获取应该支持的所有类型,然后我用 C# 编写了所有方法,并将 object 方法替换为一个针对引用类型的泛型方法,最终得到了这个类:

using System;

namespace Pfz
{
  public static class Volatile
  {
    public static byte Read(ref byte variable)
    {
      return variable;
    }
    public static double Read(ref double variable)
    {
      return variable;
    }
    public static float Read(ref float variable)
    {
      return variable;
    }
    public static int Read(ref int variable)
    {
      return variable;
    }
    public static IntPtr Read(ref IntPtr variable)
    {
      return variable;
    }
    public static long Read(ref long variable)
    {
      return variable;
    }
    public static T Read<T>(ref T variable)
    where
      T: class
    {
      return variable;
    }
    public static sbyte Read(ref sbyte variable)
    {
      return variable;
    }
    public static short Read(ref short variable)
    {
      return variable;
    }
    public static uint Read(ref uint variable)
    {
      return variable;
    }
    public static UIntPtr Read(ref UIntPtr variable)
    {
      return variable;
    }
    public static ulong Read(ref ulong variable)
    {
      return variable;
    }
    public static ushort Read(ref ushort variable)
    {
      return variable;
    }

    public static void Write(ref byte variable, byte value)
    {
      variable = value;
    }
    public static void Write(ref double variable, double value)
    {
      variable = value;
    }
    public static void Write(ref float variable, float value)
    {
      variable = value;
    }
    public static void Write(ref int variable, int value)
    {
      variable = value;
    }
    public static void Write(ref IntPtr variable, IntPtr value)
    {
      variable = value;
    }
    public static void Write(ref long variable, long value)
    {
      variable = value;
    }
    public static void Write<T>(ref T variable, T value)
    where
      T: class
    {
      variable = value;
    }
    public static void Write(ref sbyte variable, sbyte value)
    {
      variable = value;
    }
    public static void Write(ref short variable, short value)
    {
      variable = value;
    }
    public static void Write(ref uint variable, uint value)
    {
      variable = value;
    }
    public static void Write(ref UIntPtr variable, UIntPtr value)
    {
      variable = value;
    }
    public static void Write(ref ulong variable, ulong value)
    {
      variable = value;
    }
    public static void Write(ref ushort variable, ushort value)
    {
      variable = value;
    }
  }
}

最后,我重复了编译代码、执行 ildasm 转储所有库代码的过程,纠正了所有方法的 maxstacksize,并在所有加载和存储操作前加上了 volatile. 前缀,然后重新编译了库。

所以,考虑到它的简便性,我不知道为什么 .NET 中花了这么长时间才出现一个等效的类(它直到 .NET 4.5 才出现)。而且,如果我看得没错,.NET 的实现并不是用 IL 实现的,它所有的 C# 方法都被标记为 external。我认为那不是必需的。我唯一真正的问题是,我无法将一个 IL 单元编译成 C# 库的一部分,我不喜欢为一个类而有一个完整的库,我也不想修改 .targets 文件或使用 ILMerge 将 Volatile 类放入我的主库中。不过,在 .NET 4.5 中我将不再需要这个类了,所以暂时就让它作为一个单独的程序集存在吧。

关注点

IL 允许我们做许多 C# 无法做到的事情。我真的不理解这一点,因为 C# 是主要的 .NET 语言。在这种情况下,我能够解决问题,但我认为如果 volatile 关键字可以像 int x = volatile(variable);volatile(variable) = x; 这样使用,那会更简单。

不幸的是,我们不能总是使用 IL 并构建另一个库来解决我们的问题(例如,在 C# 中不可能创建模块初始化程序,而这是不能存在于另一个 DLL 中的东西)。

好了,我希望这篇文章至少能让那些想探索 .NET 极限的人感兴趣。所以,如果在 C# 中有些事情看起来是不可能的,看看 IL,也许这只是 C# 的限制,而不是 .NET 的限制。

© . All rights reserved.