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.
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); }
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.