Compartilhar via


Serialização de tipos imutáveis no Orleans

O Orleans tem um recurso que pode ser usado para evitar parte da sobrecarga associada à serialização de mensagens que contêm tipos imutáveis. Esta seção descreve o recurso e sua aplicação, começando com o contexto em que ele é relevante.

Serialização em Orleans

Quando um método de granularidade é invocado, o runtime do Orleans faz uma cópia em profundidade dos argumentos do método e cria a solicitação usando as cópias. Isso protege contra o código de chamada modificando os objetos do argumento antes que os dados sejam passados para a granularidade chamada.

Se a granularidade chamada estiver em um silo diferente, as cópias serão eventualmente serializadas em um fluxo de bytes e enviadas pela rede para o silo de destino, onde serão desserializadas novamente em objetos. Se a granularidade chamada estiver no mesmo silo, as cópias serão entregues diretamente ao método chamado.

Os valores retornados são tratados da mesma maneira: primeiro copiados, possivelmente serializados e desserializados.

Observe que todos os três processos, cópia, serialização e desserialização, respeitam a identidade do objeto. Em outras palavras, se você passar uma lista que tem o mesmo objeto duas vezes, você receberá uma lista no lado receptor contendo o mesmo objeto duas vezes, em vez de dois objetos com os mesmos valores.

Otimizar a cópia

Em muitos casos, a cópia profunda é desnecessária. Por exemplo, um cenário possível é um front-end da Web que recebe uma matriz de bytes de seu cliente e passa essa solicitação, incluindo a matriz de bytes, em uma granularidade para processamento. O processo de front-end não faz nada com a matriz depois de passar para a granularidade e, em particular, ele não reutiliza a matriz para receber uma solicitação futura. Dentro da granularidade, a matriz de bytes é analisada para buscar os dados de entrada, mas não modificada. A granularidade retorna outra matriz de bytes que ela criou para ser passada de volta para o cliente Web e ela descarta a matriz assim que a retorna. O front-end da Web passa a matriz de bytes resultante de volta para seu cliente, sem modificação.

Nesse cenário, não há necessidade de copiar as matrizes de bytes de solicitação ou resposta. Infelizmente, o runtime do Orleans não pode descobrir isso sozinho, pois não pode definir se as matrizes são modificadas depois pelo front-end da Web ou pela granularidade. Na melhor das hipóteses, precisaríamos de algum tipo de mecanismo .NET para indicar que um valor não é mais modificado; sem isso, adicionamos mecanismos específicos do Orleans para isso: a classe wrapper Immutable<T> e o ImmutableAttribute.

Use o atributo [Immutable] para tornar imutável um tipo, parâmetro, propriedade ou campo

Para tipos definidos pelo usuário, o ImmutableAttribute pode ser adicionado ao tipo. Isso instrui o serializador do Orleans a evitar copiar instâncias desse tipo. O snippet de código a seguir demonstra o uso de [Immutable] para indicar um tipo imutável. Esse tipo não será copiado durante a transmissão.

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

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

Às vezes, talvez você não tenha controle sobre o objeto, por exemplo, pode ser um List<int> que você está enviando entre granularidades. Outras vezes, talvez partes de seus objetos sejam imutáveis e outras partes não. Nesses casos, o Orleans dá suporte a opções adicionais.

  1. As assinaturas de método podem incluir ImmutableAttribute em uma base por parâmetro:

    public interface ISummerGrain : IGrain
    {
      // `values` will not be copied.
      ValueTask<int> Sum([Immutable] List<int> values);
    }
    
  2. Propriedades e campos individuais podem ser marcados como ImmutableAttribute para evitar que cópias sejam feitas quando instâncias que contêm tipo são copiadas.

    [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>.

A classe wrapper Immutable<T> é usada para indicar que um valor pode ser considerado imutável, ou seja, o valor subjacente não será modificado, portanto, nenhuma cópia será necessária para compartilhamento seguro. Observe que usar Immutable<T> implica que nem o provedor do valor e nem o destinatário do valor o modificarão no futuro. Isso não é um compromisso unilateral, mas sim um compromisso mútuo de dois lados.

Para usar Immutable<T> em sua interface de granularidade em vez de passar T, passe Immutable<T>. Por exemplo, no cenário descrito acima, o método de granularidade era:

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

O que nesse caso se tornaria:

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

Para criar um Immutable<T>, basta usar o construtor:

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

Para obter os valores dentro do tipo imutável, use a propriedade .Value:

byte[] buffer = immutable.Value;

Imutabilidade em Orleans

Para o propósito do Orleans, a imutabilidade é uma instrução bastante estrita: o conteúdo do item de dados não será modificado de qualquer forma que possa alterar o significado semântico do item ou interferir em outro thread que acessa simultaneamente o item. A maneira mais segura de garantir isso é simplesmente não modificar o item: imutabilidade bit a bit, em vez de imutabilidade lógica.

Em alguns casos, é seguro deixá-la menos estrita para a imutabilidade lógica, mas tome cuidado para garantir que o código mutável seja adequadamente thread-safe. Como lidar com multithreading é algo complexo e incomum em um contexto do Orleans, recomendamos fortemente essa abordagem e recomendamos manter a imutabilidade de bits.