Nota
El acceso a esta página requiere autorización. Puede intentar iniciar sesión o cambiar directorios.
El acceso a esta página requiere autorización. Puede intentar cambiar los directorios.
Las activaciones de grano tienen un modelo de ejecución de un solo subproceso . De forma predeterminada, procesan cada solicitud desde el principio hasta la finalización antes de que la siguiente solicitud pueda comenzar a procesarse. En algunas circunstancias, podría ser conveniente que una activación procese otras solicitudes mientras una solicitud espera a que se complete una operación asincrónica. Por este y otros motivos, Orleans proporciona algún control sobre el comportamiento de intercalación de solicitudes, tal como se describe en la sección Reentrancy. A continuación, se muestra un ejemplo de programación de solicitudes no reentrantes, que es el comportamiento predeterminado en Orleans.
Considere la siguiente definición de PingGrain
:
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");
}
}
Dos granos de tipo PingGrain
están implicados en nuestro ejemplo, A y B. Un llamante hace la siguiente llamada:
var a = grainFactory.GetGrain("A");
var b = grainFactory.GetGrain("B");
await a.CallOther(b);
El flujo de ejecución es el siguiente:
- La llamada llega a A, que registra
"1"
y, a continuación, emite una llamada a B. -
B vuelve inmediatamente de
Ping()
a A. -
A registra
"2"
y vuelve al autor de la llamada original.
Aunque A espera la llamada a B, no puede procesar ninguna solicitud entrante. Como resultado, si A y B se llamaran simultáneamente, podrían quedar bloqueados mientras esperan que esas llamadas se completen. Este ejemplo se basa en el cliente que emite la siguiente llamada:
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: Las llamadas no se bloquean
En este ejemplo:
- La llamada
Ping()
de A llega a B antes de que la llamadaCallOther(a)
llegue a B. - Por tanto, B procesa la llamada
Ping()
antes de la llamadaCallOther(a)
. - Dado que B procesa la llamada
Ping()
, A puede volver al autor de la llamada. - Cuando B emite la llamada
Ping()
a A, A sigue ocupado registrando su mensaje ("2"
), por lo que la llamada tiene que esperar un poco, pero pronto se podrá procesar. -
A procesa la llamada
Ping()
y vuelve a B, que vuelve al autor de la llamada original.
Considere una sucesión de eventos desafortunada en la que el mismo código da como resultado un interbloqueo debido a una ligera variación en el tiempo.
Caso 2: El bloqueo de llamadas
En este ejemplo:
- Las llamadas
CallOther
llegan a sus respectivos granos y se procesan de forma simultánea. - Ambos granos registran
"1"
y continúan conawait other.Ping()
. - Puesto que ambos granos siguen estando ocupados (procesando la
CallOther
solicitud, que aún no ha finalizado), lasPing()
solicitudes esperan. - Después de un tiempo, Orleans determina que la llamada ha agotado el tiempo de espera y que cada llamada
Ping()
produce una excepción. - El
CallOther
cuerpo del método no controla la excepción y se propaga hasta el autor de la llamada original.
En la sección siguiente se describe cómo evitar interbloqueos al permitir que varias solicitudes intercalen su ejecución.
Reentrada
Orleans el valor predeterminado es un flujo de ejecución seguro en el que el estado interno de un grano no se modifica simultáneamente mediante varias solicitudes. La modificación simultánea complica la lógica y supone una mayor carga para usted, el desarrollador. Esta protección contra errores de simultaneidad tiene un costo, principalmente vida: ciertos patrones de llamada pueden provocar interbloqueos, como se ha descrito anteriormente. Una manera de evitar interbloqueos es asegurarse de que las invocaciones de grano no formen nunca un ciclo. A menudo, es difícil escribir código que sea libre de ciclos y que garantice la ausencia de interbloqueos. Esperar a que cada solicitud se ejecute desde el principio hasta la finalización antes de procesar el siguiente también puede afectar al rendimiento. Por ejemplo, de forma predeterminada, si un método de grano realiza una solicitud asincrónica a un servicio de base de datos, el grano pausa la ejecución de la solicitud hasta que llega la respuesta de la base de datos.
Cada uno de estos casos se describe en las secciones siguientes. Por estos motivos, Orleans proporciona opciones para permitir que algunas o todas las solicitudes se ejecuten simultáneamente, intercalando su ejecución. En Orleans, nos referimos a cuestiones como la reentrada o la intercalación. Al ejecutar solicitudes simultáneamente, los granos que realizan operaciones asincrónicas pueden procesar más solicitudes en un período más corto.
Se podrán intercalar múltiples solicitudes en los siguientes casos:
- La clase de grano se marca con ReentrantAttribute.
- El método de interfaz se marca con AlwaysInterleaveAttribute.
- El predicado MayInterleaveAttribute del grano devuelve
true
.
Con la reentrada, el siguiente caso se convierte en una ejecución válida, quitando la posibilidad del interbloqueo descrito anteriormente.
Caso 3: el grano o el método es reentrante
En este ejemplo, los actores A y B pueden llamarse mutuamente al mismo tiempo sin riesgo de interbloqueos en la programación de solicitudes, porque ambos actores son reentrantes. En las siguientes secciones se proporciona más información sobre la reentrada.
Granos reentrantes
Puede marcar las clases de implementación Grain con ReentrantAttribute para indicar que diferentes solicitudes pueden ser intercaladas libremente.
En otras palabras, una activación reentrante podría iniciar el procesamiento de otra solicitud mientras que una solicitud anterior no ha finalizado. La ejecución todavía está limitada a un único subproceso, por lo que la activación se ejecuta un turno a la vez y cada turno se ejecuta en nombre de solo una de las solicitudes de la activación.
El código de grano reentrante nunca ejecuta varios fragmentos de código de grano en paralelo (la ejecución siempre se realiza en un único hilo), pero los granos reentrantes pueden ver que la ejecución del código para diferentes solicitudes se entrecruza. Es decir, los giros de diferentes solicitudes podrían entrelazarse.
Por ejemplo, como se muestra en el seudocódigo siguiente, tenga en cuenta que Foo
y Bar
son dos métodos de la misma clase de grano:
Task Foo()
{
await task1; // line 1
return Do2(); // line 2
}
Task Bar()
{
await task2; // line 3
return Do2(); // line 4
}
Si este grano está marcado como ReentrantAttribute, la ejecución de Foo
y Bar
podría intercalarse.
Por ejemplo, es posible el siguiente orden de ejecución:
Línea 1, línea 3, línea 2 y línea 4. Es decir, los turnos de las distintas solicitudes se intercalan.
Si el grano no fuera reentrante, las únicas ejecuciones posibles serían: línea 1, línea 2, línea 3, línea 4, o bien línea 3, línea 4, línea 1, línea 2 (no se puede iniciar una nueva solicitud antes de que finalice la anterior).
La principal desventaja al elegir entre granos reentrantes y no reentrantes es la complejidad del código necesaria para que la intercalación funcione correctamente y la dificultad de razonamiento sobre él.
En un caso trivial en el que los granos no tienen estado y la lógica es sencilla, el uso de menos granos reentrantes (pero no demasiado pocos, lo que garantiza que se utilicen todos los subprocesos de hardware) generalmente debería ser ligeramente más eficaz.
Si el código es más complejo, el uso de un mayor número de granos no reentrantes, incluso si es ligeramente menos eficaz en general, podría ahorrarle problemas importantes en la depuración de problemas de intercalación no obvios.
Al final, la respuesta depende de los detalles de la aplicación.
Métodos de intercalación
Los métodos de interfaz de grano marcados con AlwaysInterleaveAttribute siempre intercalan cualquier otra solicitud y pueden ser intercalados por cualquier otra solicitud, incluso solicitudes para métodos no-[AlwaysInterleave]
.
Considere el ejemplo siguiente:
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));
}
}
Tenga en cuenta el flujo de llamadas que inicia la siguiente solicitud de 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());
Las llamadas a GoSlow
no se intercalan, por lo que el tiempo total de ejecución de las dos GoSlow
llamadas es de aproximadamente 20 segundos. Por otro lado, GoFast
está marcado como AlwaysInterleaveAttribute. Las tres llamadas se ejecutan simultáneamente, completándose en un total de aproximadamente 10 segundos en lugar de requerir al menos 30 segundos.
Métodos de solo lectura
Cuando un método de grano no modifica el estado de grano, es seguro que se ejecute simultáneamente con otras solicitudes.
ReadOnlyAttribute indica que un método no modifica el estado del grano. Marcar métodos como ReadOnly
permite Orleans procesar la solicitud simultáneamente con otras ReadOnly
solicitudes, lo que puede mejorar significativamente el rendimiento de la aplicación. Considere el ejemplo siguiente:
public interface IMyGrain : IGrainWithIntegerKey
{
Task<int> IncrementCount(int incrementBy);
[ReadOnly]
Task<int> GetCount();
}
El GetCount
método no modifica el estado de grano, por lo que está marcado como ReadOnly
. Los autores de llamadas que esperan esta invocación de método no quedan bloqueados por otras solicitudes ReadOnly
al grano, y el método se devuelve inmediatamente.
Reentrada de la cadena de llamadas
Si un grain llama a un método en otro grain, que luego vuelve a llamar al grain original, la llamada da como resultado un interbloqueo a menos que la llamada sea reentrante. Puede habilitar la reentrada por sitio de llamada mediante la reentrada de la cadena de llamadas. Para habilitar la reentrada de la cadena de llamadas, llame al método AllowCallChainReentrancy(). Este método devuelve un valor que permite reentrada desde cualquier llamador más abajo en la cadena de llamadas hasta que el valor se elimine. Esto incluye la reentrada desde el grano llamando al método en sí. Considere el ejemplo siguiente:
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>());
}
}
En el ejemplo anterior, UserGrain.JoinRoom(roomName)
llama a ChatRoomGrain.OnJoinRoom(user)
, que intenta llamar de nuevo a UserGrain.GetDisplayName()
para obtener el nombre de visualización del usuario. Dado que esta cadena de llamadas implica un ciclo, se produce un interbloqueo si UserGrain
no permite la reentrancia mediante uno de los mecanismos admitidos descritos en este artículo. En esta instancia, usamos AllowCallChainReentrancy(), que solo permite que roomGrain
vuelva a llamar a UserGrain
. Esto le proporciona un control específico sobre dónde y cómo está habilitada la reentrada.
Si evitaras el interbloqueo anotando en su lugar la declaración del método GetDisplayName()
en IUserGrain
con [AlwaysInterleave]
, permitirías que cualquier proceso intercale una llamada con cualquier otro método. Mediante AllowCallChainReentrancy
, solo se permite llamar a métodos en , y solo hasta que roomGrain
se desecha.
Suprimir la reentrada de la cadena de llamadas
También puede suprimir la reentrada de la cadena de llamadas mediante el SuppressCallChainReentrancy() método . Esto tiene una utilidad limitada para los desarrolladores finales, pero es importante para el uso interno de las bibliotecas que amplían Orleans la funcionalidad de grano, como los canales de transmisión y difusión, para garantizar que los desarrolladores conserven el control total sobre cuándo está habilitada la reentrada de la cadena de llamadas.
Reentrada mediante un predicado
Las clases de grano pueden especificar un predicado para determinar la intercalación llamada por llamada mediante la inspección de la solicitud. El atributo [MayInterleave(string methodName)]
proporciona esta funcionalidad. El argumento para el atributo es el nombre de un método estático dentro de la clase de grano. Este método acepta un InvokeMethodRequest objeto y devuelve un bool
valor que indica si la solicitud debe intercalarse.
En este ejemplo se permite intercalar si el tipo de argumento de solicitud tiene el 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.
}
}