Orleans 中不可变类型的序列化

可以使用 Orleans 的一个功能来避免与序列化包含不可变类型的消息相关的一些开销。 本部分介绍该功能及其应用,从与之相关的上下文开始。

Orleans 中的序列化

当调用 grain 方法时,Orleans 运行时会生成方法参数的深层副本,并从副本构成请求。 这可以防止调用代码在将数据传递给被调用的 grain 之前,修改参数对象。

如果被调用的 grain 位于不同的 silo 上,则副本最终被序列化为字节流,并通过网络发送到目标 silo,在那里它们被反序列化为对象。 如果被调用的 grain 位于同一个 silo 上,则副本将直接传递给被调用的方法。

返回值的处理方式相同:首先是复制,然后可能是序列化和反序列化。

请注意,所有 3 个进程(复制、序列化和反序列化)都遵循对象标识。 换言之,如果传递一个列表,其中包含两个相同对象,则在接收端将得到一个列表,其中也包含两个相同对象,而不是两个具有相同值的对象。

优化复制

在许多情况下,深层复制是不必要的。 例如,一种可能的情况是 Web 前端从其客户端接收一个字节数组,并将该请求(包括字节数组)传递给一个 grain 进行处理。 前端进程一旦将数组传递给 grain,就不会对数组执行任何操作;具体而言,它不会重用该数组来接收将来的请求。 在 grain 内部,分析字节数组以提取输入数据,但不进行修改。 grain 返回它创建的另一个字节数组,以传递回 Web 客户端;它一返回该数据就将其丢弃。 Web 前端将结果字节数组传递回其客户端,且不进行修改。

在这种情况下,不需要复制请求或响应字节数组。 遗憾的是,Orleans 运行时无法自行解决此问题,因为它无法判断数组是否会在以后由 Web 前端或 grain 进行修改。 在所有可能的世界中,有某种 .NET 机制会指示某个值不再被修改;因为没有这种机制,我们为此添加了特定于 Orleans 的机制:Immutable<T> 包装类和 ImmutableAttribute

使用 [Immutable] 特性使类型、参数、属性或字段不可变

对于用户定义的类型,可以向该类型添加 ImmutableAttribute。 这指示 Orleans 的序列化程序避免复制此类型的实例。 以下代码片段演示如何使用 [Immutable] 表示不可变类型。 传输过程中不会复制此类型。

[Immutable]
public class MyImmutableType
{
    public int MyValue { get; }

    public MyImmutableType(int value)
    {
        MyValue = value;
    }
}

有时,你可能无法控制对象,例如,它可能是在粒度之间发送的 List<int>。 其他时候,对象的某些部分可能是不可变的,而其他部分则可变。 对于这些情况,Orleans 支持其他选项。

  1. 方法签名可以基于每个参数包含 ImmutableAttribute

    public interface ISummerGrain : IGrain
    {
      // `values` will not be copied.
      ValueTask<int> Sum([Immutable] List<int> values);
    }
    
  2. 单个属性和字段可以标记为 ImmutableAttribute,以防止在复制包含类型的实例时进行复制。

    [GenerateSerializer]
    public sealed class MyType
    {
        [Id(0), Immutable]
        public List<int> ReferenceData { get; set; }
    
        [Id(1)]
        public List<int> RunningTotals { get; set; }
    }
    

使用 Immutable<T>

Immutable<T> 包装类用于指示一个值可以被视为不可变;也就是说,基础值不会被修改,因此安全共享数据并不需要复制。 请注意,使用 Immutable<T> 意味着值的提供者和值的接收者都不会在将来修改它;它不是单方面的承诺,而是双方的共同承诺。

若要在 grain 接口中使用 Immutable<T>,请不要传递 T,而是传递 Immutable<T>。 例如,在上述场景中,grain 方法是:

Task<byte[]> ProcessRequest(byte[] request);

然后变为:

Task<Immutable<byte[]>> ProcessRequest(Immutable<byte[]> request);

要创建一个 Immutable<T>,只需使用构造函数:

Immutable<byte[]> immutable = new(buffer);

若要获取不可变类型中的值,请使用以下 .Value 属性:

byte[] buffer = immutable.Value;

Orleans 不可变性

对于 Orleans 而言,不可变性是一个相当严格的声明:数据项的内容不会以任何可能改变该项语义的方式进行修改,也不会干扰另一个线程同时访问该项。 确保这一点的最安全方法是完全不修改该项:按位不可变性,而不是逻辑不可变性。

在某些情况下,将其放宽到逻辑不可变性是安全的,但必须注意确保变化的代码是线程安全的。 因为处理多线程很复杂,而且在 Orleans 的上下文中并不常见,强烈建议不要采用此方法,建议坚持使用按位不可变性。