次の方法で共有


Orleans での不変型のシリアル化

Orleans には、不変型を含むメッセージのシリアル化に関連するオーバーヘッドを一部回避するために使用できる機能があります。 このセクションでは、その機能と応用について、まずはそれが関連する場所についてのコンテキストから説明します。

Orleans でのシリアル化

グレイン メソッドが呼び出されると、Orleans ランタイムはメソッド引数のディープ コピーを作成し、そのコピーから要求を形成します。 これにより、呼び出されたグレインにデータが渡される前に、引数オブジェクトを変更する呼び出し元のコードから保護されます。

呼び出されたグレインが別のサイロにある場合、コピーは最終的にバイト ストリームにシリアル化され、ネットワーク経由でターゲット サイロに送信され、そこでオブジェクトに逆シリアル化されます。 呼び出されたグレインが同じサイロにある場合、コピーは呼び出されたメソッドに直接渡されます。

戻り値も同じように処理されます。つまり、最初にコピーされた後、シリアル化および逆シリアル化される場合があります。

コピー、シリアル化、逆シリアル化の 3 つのプロセスすべてで、オブジェクトの同一性が守られることに注意してください。 つまり、同じオブジェクトが 2 回含まれるリストを渡した場合、受信側では、同じ値を持つ 2 つのオブジェクトではなく、同じオブジェクトが 2 回含まれるリストを取得します。

コピーの最適化

多くの場合、ディープ コピーは不要です。 たとえば、考えられるシナリオとしては、クライアントからバイト配列を受け取り、その要求 (バイト配列を含む) をグレインに渡して処理する Web フロントエンドがあります。 フロントエンド プロセスでは、配列をグレインに渡した後は、配列に対して何も行いません。特に、その配列を再利用して将来の要求を受け取ることはありません。 グレイン内では、バイト配列が解析されて入力データがフェッチされますが、変更はされません。 グレインは、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> を使用するには、T を渡す代わりに Immutable<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 のコンテキストでは一般的ではないため、このアプローチは取らず、ビット単位の不変性を守り続けることを強くお勧めします。