Serialização de tipos imutáveis em Orleans
Orleans tem um recurso que pode ser usado para evitar parte da sobrecarga associada à serialização de mensagens contendo tipos imutáveis. Esta seção descreve o recurso e sua aplicação, começando com o contexto sobre onde ele é relevante.
Serialização em Orleans
Quando um método grain é invocado, o Orleans tempo de execução faz uma cópia profunda dos argumentos do método e forma a solicitação a partir das cópias. Isso protege contra o código de chamada modificando os objetos de argumento antes que os dados sejam passados para o grão chamado.
Se o grão chamado 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 o grão chamado estiver no mesmo silo, então as cópias são entregues diretamente ao método chamado.
Os valores de retorno são tratados da mesma maneira: primeiro copiados, depois possivelmente serializados e desserializados.
Observe que todos os 3 processos, cópia, serialização e desserialização, respeitam a identidade do objeto. Em outras palavras, se você passar uma lista que tenha o mesmo objeto nela duas vezes, no lado recetor você obterá uma lista com o mesmo objeto nela duas vezes, em vez de com dois objetos com os mesmos valores neles.
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, para um grão para processamento. O processo de front-end não faz nada com o array depois de passá-lo para o grão; em particular, ele não reutiliza o array para receber uma solicitação futura. Dentro do grão, a matriz de bytes é analisada para buscar os dados de entrada, mas não modificada. O grão retorna outra matriz de bytes que ele criou para ser passado de volta para o web client; ele descarta a matriz assim que a retorna. O front-end da Web passa a matriz de bytes de resultado 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 Orleans tempo de execução não pode descobrir isso por si só, uma vez que não pode dizer se os arrays são modificados posteriormente pelo front-end da web ou pelo grão. No melhor dos mundos possíveis, teríamos algum tipo de mecanismo .NET para indicar que um valor não é mais modificado; Na falta disso, adicionamos Orleansmecanismos específicos para isso: a Immutable<T> classe wrapper e o ImmutableAttribute.
Use o [Immutable]
atributo para tornar um tipo, parâmetro, propriedade ou campo como imutável
Para tipos definidos pelo usuário, o ImmutableAttribute pode ser adicionado ao tipo. Isso instrui ' Orleansserializer a evitar copiar instâncias desse tipo.
O trecho de código a seguir demonstra o uso [Immutable]
para denotar um tipo imutável. Este 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, você pode não ter controle sobre o objeto, por exemplo, pode ser um List<int>
que você está enviando entre grãos. Outras vezes, talvez partes de seus objetos sejam imutáveis e outras não. Para estes casos, Orleans suporta opções adicionais.
As assinaturas de método podem incluir ImmutableAttribute , 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 para ImmutableAttribute impedir que cópias sejam feitas quando instâncias do tipo que contém 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; } }
Utilizar o comando Immutable<T>
A Immutable<T> classe wrapper é usada para indicar que um valor pode ser considerado imutável, ou seja, o valor subjacente não será modificado, portanto, nenhuma cópia é necessária para o compartilhamento seguro. Note-se que o uso Immutable<T>
implica que nem o fornecedor do valor nem o destinatário do valor irão modificá-lo no futuro, não é um compromisso unilateral, mas sim um compromisso mútuo dual-side.
Para usar Immutable<T>
na sua interface de grãos, em vez de passar T
, passe Immutable<T>
. Por exemplo, no cenário acima descrito, o método de grãos foi:
Task<byte[]> ProcessRequest(byte[] request);
Que se tornariam então:
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 imutável, use a .Value
propriedade:
byte[] buffer = immutable.Value;
Imutabilidade em Orleans
Para Orleans' propósitos, a imutabilidade é uma declaraçã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 que interfira com outro thread acessando simultaneamente o item. A maneira mais segura de garantir isso é simplesmente não modificar o item: imutabilidade bitwise, em vez de imutabilidade lógica.
Em alguns casos, é seguro relaxar isso para a imutabilidade lógica, mas deve-se tomar cuidado para garantir que o código mutante seja adequadamente thread-safe. Como lidar com multithreading é complexo e incomum em um Orleans contexto, recomendamos fortemente contra essa abordagem e recomendamos manter a imutabilidade bitwise.