Compartir a través de


Serialización de tipos inmutables en Orleans

Orleans tiene una característica que se puede usar para evitar parte de la sobrecarga asociada con la serialización de mensajes que contienen tipos inmutables. En esta sección se describe la característica y su aplicación, empezando con el contexto en el que es pertinente.

Serialización en Orleans

Cuando se invoca un método de grano, el runtime de Orleans realiza una copia en profundidad de los argumentos del método y forma la solicitud a partir de las copias. Esto impide que el código de llamada modifique los objetos de argumento antes de que los datos se pasen al grano llamado.

Si el grano llamado se encuentra en un silo diferente, las copias se serializan finalmente en una secuencia de bytes y se envían a través de la red al silo de destino, donde se deserializan de nuevo en objetos. Si el grano llamado está en el mismo silo, las copias se entregan directamente al método llamado.

Los valores devueltos se controlan de la misma manera: primero se copian y, luego, posiblemente se serializan y deserializan.

Tenga en cuenta que los tres procesos (copiar, serializar y deserializar) respetan la identidad del objeto. En otras palabras, si pasa una lista que contiene el mismo objeto dos veces, en el lado receptor obtendrá una lista con el mismo objeto dos veces, en lugar de con dos objetos con los mismos valores.

Optimización de la copia

En muchos casos, no es necesaria una copia en profundidad. Por ejemplo, un escenario posible es un front-end web que recibe una matriz de bytes de su cliente y pasa esa solicitud, incluida la matriz de bytes, a un grano para su procesamiento. El proceso de front-end no hace nada con la matriz una vez que la ha pasado al grano; en concreto, no la reutiliza para recibir una solicitud futura. Dentro del grano, la matriz de bytes se analiza para capturar los datos de entrada, pero no se modifica. El grano devuelve otra matriz de bytes que ha creado para pasarla al cliente web y descarta la matriz en cuanto la devuelve. El front-end web pasa la matriz de bytes resultante a su cliente, sin realizar ninguna modificación.

En este escenario, no es necesario copiar las matrices de bytes de solicitud o respuesta. Desafortunadamente, el runtime de Orleans no puede averiguar esto por sí mismo, ya que no puede saber si el front-end web o el grano modifican más adelante las matrices. En un mundo ideal existiría algún tipo de mecanismo de .NET para indicar que un valor ya no se modifica, pero a falta de esto, hemos agregado mecanismos específicos de Orleans para conseguirlo: la clase contenedora Immutable<T> y ImmutableAttribute.

Use el atributo [Immutable] para convertir un tipo, un parámetro, una propiedad o un campo como inmutable

En el caso de los tipos definidos por el usuario, se puede agregar ImmutableAttribute al tipo. Esto indica al serializador de Orleans que evite copiar instancias de este tipo. En el fragmento de código siguiente se muestra el uso de [Immutable] para indicar un tipo inmutable. Este tipo no se copiará durante la transmisión.

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

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

A veces, es posible que no tenga control sobre el objeto, por ejemplo, puede ser un List<int> que se envía entre granos. Otras veces, es posible que unas partes de los objetos sean inmutables y otras no. En estos casos, Orleans admite opciones adicionales.

  1. Las firmas de método pueden incluir ImmutableAttribute en función de cada parámetro:

    public interface ISummerGrain : IGrain
    {
      // `values` will not be copied.
      ValueTask<int> Sum([Immutable] List<int> values);
    }
    
  2. Las propiedades y campos individuales se pueden marcar como ImmutableAttribute para evitar que se realicen copias cuando se copian instancias del tipo contenedor.

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

Use Immutable<T>

La clase contenedora Immutable<T> se usa para indicar que un valor se puede considerar inmutable; es decir, el valor subyacente no se modificará, por lo que no es necesario copiarlo para un uso compartido seguro. Tenga en cuenta que el uso de Immutable<T> implica que ni el proveedor del valor ni el destinatario del valor lo modificarán en el futuro; no es un compromiso unilateral, sino más bien un compromiso mutuo.

Para usar Immutable<T> en la interfaz de grano, en lugar de pasar T, pase Immutable<T>. Por ejemplo, en el escenario descrito anteriormente, el método de grano era:

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

Después se convierte en lo siguiente:

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

Para crear Immutable<T>, simplemente use el constructor:

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

Para obtener los valores del tipo inmutable, use la propiedad .Value:

byte[] buffer = immutable.Value;

Inmutabilidad en Orleans

Para los fines de Orleans, la inmutabilidad es una instrucción bastante estricta. El contenido del elemento de datos no se modificará de ninguna manera que pueda cambiar el significado semántico del elemento o que interfiera con otro subproceso que acceda simultáneamente al elemento. La manera más segura de garantizar esto es simplemente no modificar en absoluto el elemento; es decir, usar la inmutabilidad bit a bit, en lugar de la inmutabilidad lógica.

En algunos casos, es seguro relajar esto a la inmutabilidad lógica, pero se debe tener cuidado para asegurarse de que el código de mutación sea correctamente seguro para subprocesos. Dado que el tratamiento con multithreading es complejo y poco frecuente en un contexto de Orleans, se recomienda encarecidamente usar este enfoque y recomendar la inmutabilidad bit a bit.