Introducción a la programación
Hay dos formas de programación en Orleans que son importantes para los granos:
- Programación de solicitudes: programación de llamadas de los granos entrantes para su ejecución según las reglas de programación descritas en Programación de solicitudes.
- Programación de tareas, programación de bloques sincrónicos de código que se ejecutarán como un subproceso único
Todo el código del grano se ejecuta en el programador de tareas de este, lo que significa que las solicitudes también se ejecutan en el programador de tareas del grano. Incluso si las reglas de programación de solicitudes permiten que varias solicitudes se ejecuten simultáneamente, no se ejecutarán en paralelo ya que el programador de tareas del grano siempre ejecuta las tareas una a una y, por lo tanto, nunca ejecuta varias tareas en paralelo.
Programación de tareas
Para comprender mejor la programación, tenga en cuenta el siguiente grano, MyGrain
, que tiene un método denominado DelayExecution()
que registra un mensaje, espera un tiempo y, a continuación, registra otro mensaje antes de devolverlo.
public interface IMyGrain : IGrain
{
Task DelayExecution();
}
public class MyGrain : Grain, IMyGrain
{
private readonly ILogger<MyGrain> _logger;
public MyGrain(ILogger<MyGrain> logger) => _logger = logger;
public async Task DelayExecution()
{
_logger.LogInformation("Executing first task");
await Task.Delay(1_000);
_logger.LogInformation("Executing second task");
}
}
Cuando se ejecuta este método, el cuerpo del método se ejecutará en dos partes:
- La primera llamada a
_logger.LogInformation(...)
y la llamada aTask.Delay(1_000)
. - La segunda llamada a
_logger.LogInformation(...)
.
La segunda tarea no se programará en el programador de tareas del grano hasta que se complete la llamada a Task.Delay(1_000)
, momento en el que programará la continuación del método del grano.
Esta es una representación gráfica de cómo se programa y se ejecuta una solicitud como dos tareas:
La descripción anterior no es específica de Orleans y muestra cómo funciona la programación de tareas en .NET: el compilador convierte los métodos asincrónicos de C# en una máquina de estados asincrónica y la ejecución avanza en esta mediante pasos específicos. Cada paso se programa en el TaskScheduler actual (al que se accede mediante TaskScheduler.Current, que tiene TaskScheduler.Default como valor predeterminado) o en el SynchronizationContext actual. Si se usa un TaskScheduler
, cada paso del método se representa mediante una instancia de Task
que se pasa a ese TaskScheduler
. Por lo tanto, una Task
en .NET puede representar dos elementos conceptuales:
- Una operación asincrónica en la que se puede esperar. La ejecución del método
DelayExecution()
anterior se representa mediante unaTask
que se puede esperar. - En un bloque de trabajo sincrónico, cada fase del método
DelayExecution()
anterior se representa mediante unaTask
.
Cuando TaskScheduler.Default
está en uso, las continuaciones se programan directamente en .NET ThreadPool y no se encapsulan en un objeto Task
. El ajuste de las continuaciones en las instancias de Task
se produce de forma transparente y, por lo tanto, los desarrolladores rara vez necesitan conocer estos detalles de implementación.
Programación de tareas en Orleans
Cada activación de un grano tiene su propia instancia de TaskScheduler
, la cual es responsable de aplicar el modelo de ejecución de subproceso único de los granos. Internamente, este TaskScheduler
se implementa mediante ActivationTaskScheduler
y WorkItemGroup
. WorkItemGroup
mantiene las tareas puestas en cola en una Queue<T> donde T
es un elemento de Task
internamente e implementa IThreadPoolWorkItem. Para ejecutar cada una de las programaciones actualmente puestas en cola Task
, WorkItemGroup
se programa a sí misma en .NET ThreadPool
. Cuando el ThreadPool
de .NET invoca el método IThreadPoolWorkItem.Execute()
de WorkItemGroup
, WorkItemGroup
ejecuta las instancias de Task
puestas en cola una a una.
Cada grano tiene un programador que se ejecuta mediante la autoprogramación en el ThreadPool
de .NET:
Cada programador contiene una cola de tareas:
El ThreadPool
de .NET ejecuta cada elemento de trabajo puesto en cola. Esto incluye programadores de granos, así como otros elementos de trabajo, como los elementos de trabajo programados mediante Task.Run(...)
:
Nota:
El programador de un grano solo se puede ejecutar en un subproceso a la vez, pero no siempre se ejecuta en el mismo subproceso. El ThreadPool
de .NET es libre de usar un subproceso diferente cada vez que se ejecuta el programador del grano. El programador del grano es responsable de asegurarse de que solo se ejecuta en un subproceso cada vez y así es cómo se implementa el modelo de ejecución de un solo subproceso de los granos.