Compartir a través de


Serialización de tipos inmutables en Orleans

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

Serialización en Orleans

Cuando se invoca un método de grain, el Orleans tiempo de ejecución realiza una copia profunda de los argumentos del método y forma la solicitud a partir de estas copias. Esto protege contra el código de llamada que modifica los objetos de argumento antes de que los datos pasen al grano llamado.

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

Los valores devueltos se gestionan de la misma manera: se copian primero y luego, si es necesario, 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, el lado receptor obtiene una lista con el mismo objeto dos veces, en lugar de dos objetos con los mismos valores.

Optimización de la copia

En muchos casos, la copia en profundidad no es necesaria. Por ejemplo, considere un escenario en el que un front-end web recibe una array de bytes de su cliente y pasa esa solicitud, incluido el array de bytes, a un grain para su procesamiento. El proceso de front-end no hace nada con la matriz después de pasarla al grano; en concreto, no reutiliza la matriz para solicitudes futuras. Dentro del grano, la matriz de bytes se analiza para capturar datos de entrada, pero no se modifica. El grano devuelve otra matriz de bytes que creó al cliente web y descarta la matriz inmediatamente después de devolverla. El front-end web pasa la matriz de bytes de resultado a su cliente sin modificaciones.

En este escenario, no es necesario copiar las matrices de bytes de solicitud o respuesta. Desafortunadamente, el Orleans tiempo de ejecución no puede determinar esto automáticamente, ya que no puede precisar si el front-end web o el grain modifica las matrices posteriormente. Idealmente, un mecanismo de .NET indicaría que ya no se modifica un valor. En ausencia de eso, hemos agregado Orleans mecanismos específicos: la clase contenedora Immutable<T> y el ImmutableAttribute.

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

En el caso de los tipos definidos por el usuario, puede agregar el ImmutableAttribute al tipo. Esto indica al Orleans serializador que evite copiar instancias de este tipo. En el siguiente fragmento de código se muestra cómo usar [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 controle el objeto; por ejemplo, podría ser un List<int> que se envía entre granos. Otras veces, las partes de los objetos pueden ser inmutables, mientras que otras no. En estos casos, Orleans admite opciones adicionales.

  1. Las firmas de método pueden incluir ImmutableAttribute para cada parámetro:

    public interface ISummerGrain : IGrain
    {
      // `values` will not be copied.
      ValueTask<int> Sum([Immutable] List<int> values);
    }
    
  2. Marque propiedades y campos individuales como ImmutableAttribute para evitar 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; }
    }
    

Utilice Immutable<T>

Use la Immutable<T> clase contenedora para indicar que un valor se puede considerar inmutable; es decir, el valor subyacente no se modificará, por lo que no se requiere ninguna copia para el uso compartido seguro. Tenga en cuenta que el uso Immutable<T> no implica ni el proveedor ni el destinatario del valor lo modificarán en el futuro. Es un compromiso mutuo y de doble cara, no uno solo.

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

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

Lo cual se convertiría en:

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

Para crear un Immutable<T>, simplemente use su constructor:

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

Para obtener el valor dentro del contenedor inmutable, use la .Value propiedad :

byte[] buffer = immutable.Value;

Inmutabilidad en Orleans

Para Orleans' fines, la inmutabilidad es una declaración estricta: el contenido del elemento de datos no se modificará de ninguna manera que pueda cambiar el significado semántico del elemento o interferir con otro subproceso al mismo tiempo acceder a él. La manera más segura de garantizar esto es simplemente no modificar el elemento en absoluto: usar inmutabilidad bit a bit en lugar de inmutabilidad lógica.

En algunos casos, es seguro relajarse a la inmutabilidad lógica, pero debe tener cuidado para asegurarse de que el código modificador es seguro para los subprocesos. Dado que el tratamiento con multithreading es complejo y poco común en un Orleans contexto, desaconsejamos encarecidamente este enfoque y le recomendamos que se adhiera a la inmutabilidad a nivel de bits.