Nota
O acesso a esta página requer autorização. Pode tentar iniciar sessão ou alterar os diretórios.
O acesso a esta página requer autorização. Pode tentar alterar os diretórios.
As ativações de grãos têm um modelo de execução de thread único . Por padrão, eles processam cada solicitação do início à conclusão antes que a próxima solicitação possa começar a ser processada. Em algumas circunstâncias, pode ser desejável que uma ativação processe outras solicitações enquanto uma solicitação aguarda a conclusão de uma operação assíncrona. Por este e outros motivos, Orleans dá-lhe algum controlo sobre o comportamento de intercalação de pedidos, conforme descrito na secção Reentrância . A seguir é apresentado um exemplo de agendamento de solicitação não reentrante, que é o comportamento padrão no Orleans.
Considere a seguinte PingGrain
definição:
public interface IPingGrain : IGrainWithStringKey
{
Task Ping();
Task CallOther(IPingGrain other);
}
public class PingGrain : Grain, IPingGrain
{
private readonly ILogger<PingGrain> _logger;
public PingGrain(ILogger<PingGrain> logger) => _logger = logger;
public Task Ping() => Task.CompletedTask;
public async Task CallOther(IPingGrain other)
{
_logger.LogInformation("1");
await other.Ping();
_logger.LogInformation("2");
}
}
Dois grãos do tipo PingGrain
estão envolvidos em nosso exemplo, A e B. Um chamador invoca a seguinte chamada:
var a = grainFactory.GetGrain("A");
var b = grainFactory.GetGrain("B");
await a.CallOther(b);
O fluxo de execução é o seguinte:
- A chamada chega a A, que registra
"1"
e emite uma chamada para B. -
B retorna imediatamente do
Ping()
para A. -
Um registra
"2"
e retorna ao chamador original.
Enquanto A aguarda a chamada para B, ele não pode processar nenhuma solicitação de entrada. Como resultado, se A e B ligassem simultaneamente um ao outro, eles poderiam ficar em impasse enquanto aguardavam que essas chamadas fossem concluídas. Aqui está um exemplo, com base no cliente que emite a seguinte chamada:
var a = grainFactory.GetGrain("A");
var b = grainFactory.GetGrain("B");
// A calls B at the same time as B calls A.
// This might deadlock, depending on the non-deterministic timing of events.
await Task.WhenAll(a.CallOther(b), b.CallOther(a));
Caso 1: As chamadas não entram em impasse
Neste exemplo:
- A
Ping()
chamada de A chega a B antes de aCallOther(a)
chamada chegar a B. - Portanto, B processa a
Ping()
chamada antes daCallOther(a)
chamada. - Como B processa a
Ping()
chamada, A é capaz de retornar ao chamador. - Quando B emite sua
Ping()
chamada para A, A ainda está ocupado registrando sua mensagem ("2"
), então a chamada tem que esperar por uma curta duração, mas logo pode ser processada. -
A processa a
Ping()
chamada e retorna para B, que retorna ao chamador original.
Considere uma série de eventos menos afortunada em que o mesmo código resulta em um bloqueio devido a um tempo ligeiramente diferente.
Caso 2: O impasse das chamadas
Neste exemplo:
- As
CallOther
chamadas chegam às suas respetivas unidades e são processadas simultaneamente. - Ambos os grãos registram
"1"
e prosseguem paraawait other.Ping()
. - Como ambos os grãos ainda estão ocupados (processando o
CallOther
pedido, que ainda não terminou), osPing()
pedidos aguardam. - Depois de um tempo, Orleans determina que a chamada atingiu o tempo limite e cada
Ping()
chamada resulta em uma exceção sendo lançada. - O
CallOther
corpo do método não manipula a exceção e propaga-se até ao chamador original.
A seção a seguir descreve como evitar bloqueios permitindo que várias solicitações intercalem sua execução.
Reentrada
Orleans O padrão é um fluxo de execução seguro em que o estado interno de um grão não é modificado simultaneamente por várias solicitações. A modificação simultânea complica a lógica e coloca um fardo maior sobre você, o desenvolvedor. Essa proteção contra bugs de concorrência tem um custo, principalmente atividade: certos padrões de chamada podem levar a impasses, como discutido anteriormente. Uma maneira de evitar impasses é garantir que as chamadas de grãos nunca formem um ciclo. Muitas vezes, é difícil escrever um código que seja livre de ciclos e garantido que não entre em impasse. Esperar que cada solicitação seja executada do início à conclusão antes de processar a próxima também pode prejudicar o desempenho. Por exemplo, por padrão, se um método grain executa uma solicitação assíncrona para um serviço de banco de dados, o grain pausa a execução da solicitação até que a resposta do banco de dados chegue.
Cada um destes casos é analisado nas secções seguintes. Por esses motivos, Orleans fornece opções para permitir que algumas ou todas as solicitações sejam executadas simultaneamente, intercalando sua execução. Na Orleans, referimo-nos a preocupações como reentrância ou intercalamento. Ao executar solicitações concorrentemente, os grãos que realizam operações assíncronas podem processar mais solicitações num período mais curto.
Vários pedidos podem ser intercalados nos seguintes casos:
- A classe de grãos está marcada com ReentrantAttribute.
- O método de interface é marcado com AlwaysInterleaveAttribute.
- O predicado do MayInterleaveAttribute grão retorna
true
.
Com a reentrância, o caso seguinte torna-se uma execução válida, eliminando a possibilidade do impasse descrito acima.
Caso 3: O grão ou método é re-entrante
Neste exemplo, os grãos A e B podem ligar um para o outro simultaneamente sem possíveis bloqueios de agendamento de solicitação porque ambos os grãos são reentrantes. As secções seguintes fornecem mais detalhes sobre reentrância.
Grãos reentrantes
Você pode marcar Grain classes de implementação com o ReentrantAttribute para indicar que diferentes solicitações podem ser intercaladas livremente.
Em outras palavras, uma ativação de reentrante pode começar a processar outra solicitação enquanto uma solicitação anterior não foi concluída. A execução ainda é limitada a um único thread, de modo que a ativação é executada um turno de cada vez, e cada turno é executado em nome de apenas uma das solicitações da ativação.
O código de grão reentrante nunca executa várias partes de código de grão em paralelo (a execução é sempre de thread único), mas os grãos reentrantes podem experimentar a execução de código para diferentes solicitações intercaladas. Ou seja, os turnos de continuação de diferentes pedidos podem intercalar-se.
Por exemplo, como mostrado no pseudocódigo a seguir, considere que Foo
e Bar
são dois métodos da mesma classe de grãos:
Task Foo()
{
await task1; // line 1
return Do2(); // line 2
}
Task Bar()
{
await task2; // line 3
return Do2(); // line 4
}
Se este elemento estiver marcado ReentrantAttribute, a execução de Foo
e Bar
pode intercalar-se.
Por exemplo, é possível a seguinte ordem de execução:
Linha 1, linha 3, linha 2 e linha 4. Ou seja, as voltas de diferentes pedidos se intercalam.
Se o "grain" não fosse reentrante, as únicas execuções possíveis seriam: linha 1, linha 2, linha 3, linha 4 OU: linha 3, linha 4, linha 1, linha 2 (uma nova solicitação não pode iniciar antes que a anterior tenha terminado).
A principal contrapartida ao escolher entre grãos reentrantes e não reentrantes é a complexidade do código necessária para fazer o intercalamento funcionar corretamente e a dificuldade de raciocinar sobre isso.
Em um caso trivial em que os grãos são sem estado e a lógica é simples, usar menos grãos reentrantes (mas não muito poucos, garantindo que todos os encadeamentos de hardware sejam utilizados) geralmente deve ser um pouco mais eficiente.
Se o código for mais complexo, usar um número maior de grãos não reentrantes, mesmo que um pouco menos eficiente no geral, pode evitar problemas significativos na depuração de problemas de intercalação complexos e não óbvios.
No final, a resposta depende das especificidades da aplicação.
Métodos de intercalação
Os métodos de interface de grão marcados com AlwaysInterleaveAttribute sempre intercalam qualquer outra solicitação e sempre podem ser intercalados por qualquer outra solicitação, mesmo solicitações para métodos não-[AlwaysInterleave]
.
Considere o seguinte exemplo:
public interface ISlowpokeGrain : IGrainWithIntegerKey
{
Task GoSlow();
[AlwaysInterleave]
Task GoFast();
}
public class SlowpokeGrain : Grain, ISlowpokeGrain
{
public async Task GoSlow()
{
await Task.Delay(TimeSpan.FromSeconds(10));
}
public async Task GoFast()
{
await Task.Delay(TimeSpan.FromSeconds(10));
}
}
Considere o fluxo de chamadas iniciado pela seguinte solicitação do cliente:
var slowpoke = client.GetGrain<ISlowpokeGrain>(0);
// A. This will take around 20 seconds.
await Task.WhenAll(slowpoke.GoSlow(), slowpoke.GoSlow());
// B. This will take around 10 seconds.
await Task.WhenAll(slowpoke.GoFast(), slowpoke.GoFast(), slowpoke.GoFast());
As chamadas para GoSlow
não são intercaladas, portanto, o tempo total de execução das duas GoSlow
chamadas é de cerca de 20 segundos. Por outro lado, GoFast
está marcado AlwaysInterleaveAttribute. As três chamadas para ele são executadas simultaneamente, completando em aproximadamente 10 segundos no total, em vez de exigir pelo menos 30 segundos.
Métodos somente leitura
Quando uma função de processamento de grão não modifica o estado do grão, é seguro executá-la simultaneamente com outras requisições. O ReadOnlyAttribute indica que um método não modifica o estado do grão. Marcar métodos como ReadOnly
permite Orleans processar sua solicitação simultaneamente com outras ReadOnly
solicitações, o que pode melhorar significativamente o desempenho do seu aplicativo. Considere o seguinte exemplo:
public interface IMyGrain : IGrainWithIntegerKey
{
Task<int> IncrementCount(int incrementBy);
[ReadOnly]
Task<int> GetCount();
}
O GetCount
método não modifica o estado de grão, por isso está marcado ReadOnly
. Os chamadores que aguardam essa invocação de método não são bloqueados por outras ReadOnly
solicitações para o grão, e o método retorna imediatamente.
Reentrância da cadeia de chamadas
Se um grão chama um método em outro grão, que então chama de volta para o grão original, a chamada resulta em um impasse, a menos que a chamada seja reentrante. Você pode habilitar a reentrância por local de chamada usando a reentrância da cadeia de chamadas. Para habilitar a reentrância da cadeia de chamadas, chame o método AllowCallChainReentrancy(). Esse método retorna um valor que permite a reentrada de qualquer chamador mais abaixo na cadeia de chamadas até que o valor seja descartado. Isso inclui a reentrada do grão chamando o método em si. Considere o seguinte exemplo:
public interface IChatRoomGrain : IGrainWithStringKey
{
ValueTask OnJoinRoom(IUserGrain user);
}
public interface IUserGrain : IGrainWithStringKey
{
ValueTask JoinRoom(string roomName);
ValueTask<string> GetDisplayName();
}
public class ChatRoomGrain : Grain<List<(string DisplayName, IUserGrain User)>>, IChatRoomGrain
{
public async ValueTask OnJoinRoom(IUserGrain user)
{
var displayName = await user.GetDisplayName();
State.Add((displayName, user));
await WriteStateAsync();
}
}
public class UserGrain : Grain, IUserGrain
{
public ValueTask<string> GetDisplayName() => new(this.GetPrimaryKeyString());
public async ValueTask JoinRoom(string roomName)
{
// This prevents the call below from triggering a deadlock.
using var scope = RequestContext.AllowCallChainReentrancy();
var roomGrain = GrainFactory.GetGrain<IChatRoomGrain>(roomName);
await roomGrain.OnJoinRoom(this.AsReference<IUserGrain>());
}
}
No exemplo anterior, UserGrain.JoinRoom(roomName)
chama ChatRoomGrain.OnJoinRoom(user)
, que tenta chamar de volta UserGrain.GetDisplayName()
para obter o nome de exibição do usuário. Como essa cadeia de chamadas envolve um ciclo, ela resulta em um impasse se UserGrain
não permitir a reentrada usando um dos mecanismos suportados discutidos neste artigo. Neste caso, usamos AllowCallChainReentrancy(), que permite apenas roomGrain
chamar de volta para UserGrain
. Isso oferece um controlo refinado sobre onde e como a reentrância é permitida.
Caso evitasse o deadlock ao anotar a declaração do método GetDisplayName()
em IUserGrain
com [AlwaysInterleave]
em vez disso, permitiria que qualquer grain entrelaçasse uma chamada GetDisplayName
com qualquer outro método. Ao usar AllowCallChainReentrancy
, você permite que sóroomGrain
chame métodos no UserGrain
, e apenas até que scope
seja descartado.
Suprimir a reentrância da cadeia de chamadas
Você também pode suprimir a reentrada da cadeia de chamadas usando o SuppressCallChainReentrancy() método. Isso tem utilidade limitada para os desenvolvedores finais, mas é importante para uso interno por bibliotecas que ampliam a funcionalidade granular, como streaming e canais de difusão, para garantir que os desenvolvedores mantenham controle total sobre quando a reentrância da cadeia de chamadas está habilitada.
Reentrância usando um predicado
As classes de grãos podem especificar um predicado para determinar a intercalação uma chamada de cada vez, inspecionando a solicitação. O [MayInterleave(string methodName)]
atributo fornece essa funcionalidade. O argumento para o atributo é o nome de um método estático dentro da classe grain. Este método aceita um InvokeMethodRequest objeto e retorna uma bool
que indica se a solicitação deve ser intercalada.
Aqui está um exemplo que permite a intercalação, se o tipo de argumento de pedido tiver o atributo [Interleave]
.
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct)]
public sealed class InterleaveAttribute : Attribute { }
// Specify the may-interleave predicate.
[MayInterleave(nameof(ArgHasInterleaveAttribute))]
public class MyGrain : Grain, IMyGrain
{
public static bool ArgHasInterleaveAttribute(IInvokable req)
{
// Returning true indicates that this call should be interleaved with other calls.
// Returning false indicates the opposite.
return req.Arguments.Length == 1
&& req.Arguments[0]?.GetType()
.GetCustomAttribute<InterleaveAttribute>() != null;
}
public Task Process(object payload)
{
// Process the object.
}
}