Compartir a través de


Introducción a la programación

Hay dos formas de programación en Orleans que son importantes para los granos:

  1. 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.
  2. 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:

  1. La primera llamada a _logger.LogInformation(...) y la llamada a Task.Delay(1_000).
  2. 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:

Two-Task-based request execution example.

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:

  1. Una operación asincrónica en la que se puede esperar. La ejecución del método DelayExecution() anterior se representa mediante una Task que se puede esperar.
  2. En un bloque de trabajo sincrónico, cada fase del método DelayExecution() anterior se representa mediante una Task.

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:

Orleans grains scheduling themselves on the .NET ThreadPool.

Cada programador contiene una cola de tareas:

Scheduler queue of scheduled tasks.

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(...):

Visualization of the all schedulers running in the .NET ThreadPool.

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.