Orleans 中不可变类型的序列化

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

Orleans 中的序列化

调用粒度方法时, Orleans 运行时会生成方法参数的深层副本,并从这些副本形成请求。 这可以防止调用代码在数据传递到被调用方前修改参数对象。

如果调用的谷物位于不同的筒仓上,这些副本最终会序列化为字节流,并通过网络发送到目标筒仓,在那里它们被反序列化回对象。 如果被调用的粮仓在同一个仓库中,副本会被直接传递给被调用的方法。

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

请注意,所有三个进程(复制、序列化和反序列化)都遵循对象标识。 换句话说,如果传递包含同一对象的列表两次,接收方将获取具有相同对象的列表两次,而不是两个具有相同值的对象。

优化复制

在许多情况下,不需要深度复制。 例如,假设 Web 前端从其客户端接收字节数组,并将该请求(包括字节数组)传递到处理粒度。 前端进程在将数组传递给粒子后不对数组执行任何操作;具体而言,它不会将数组重新用于将来的请求。 在数据处理中,字节数组被解析以提取输入数据,但不会被修改。 Grain 将它创建的另一个字节数组发送回 Web 客户端,并在发送后立即丢弃该数组。 Web 前端在不修改的情况下将结果字节数组传回其客户端。

在这种情况下,无需复制请求或响应字节数组。 遗憾的是,Orleans 运行时环境无法自动弄清楚这一点,因为它无法确定是 Web 前端还是颗粒稍后会修改数组。 理想情况下,.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> 意味着提供程序和值接收方都不会在将来对其进行修改。 这是一个相互的、双方的承诺,而不是单方面的。

若要在粒度接口中使用 Immutable<T> ,请传递 Immutable<T>,而不是 T。 例如,在上述方案中,粒化方法是:

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上下文中处理多线程很复杂且不常见,因此我们强烈建议不要采用此方法,而建议坚持使用按位不可变性。