Partager via


Vue d’ensemble de la planification

Dans Orleans, il existe deux formes de planification qui se rapportent aux grains :

  1. La planification des demandes, la planification des appels de grain entrants pour l’exécution en fonction des règles de planification décrites dans Planification des demandes.
  2. La planification des tâches, la planification de blocs de code synchrones à exécuter de manière monothread

Tout le code d’un grain est exécuté sur le planificateur de tâches du grain, ce qui signifie que les demandes sont également exécutées sur le planificateur de tâches du grain. Même si les règles de planification des demandes permettant l’exécution simultanée de plusieurs demandes, elles ne s’exécutent pas en parallèle, car le planificateur de tâches du grain exécute toujours les tâches une par une et n’exécute donc jamais plusieurs tâches en parallèle.

Planification des tâches

Pour mieux comprendre la planification, tenez compte du grain suivant, MyGrain, qui possède une méthode appelée DelayExecution() qui journalise un message, attend un certain temps, puis journalise un autre message avant de retourner.

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");
    }
}

Quand cette méthode est exécutée, le corps de la méthode est exécuté en deux parties :

  1. Le premier appel à _logger.LogInformation(...) et l’appel à Task.Delay(1_000).
  2. Le second appel à _logger.LogInformation(...).

La deuxième tâche sera planifiée sur le planificateur de tâches du grain seulement quand l’appel Task.Delay(1_000) prendra fin. À ce moment, elle planifiera la continuation de la méthode du grain.

Voici une représentation graphique de la planification et de l’exécution d’une demande sous la forme de deux tâches :

Two-Task-based request execution example.

La description ci-dessus n’est pas spécifique à Orleans et présente la façon dont la planification des tâches dans .NET fonctionne : les méthodes asynchrones en C# sont converties en une machine à états asynchrone par le compilateur et l’exécution progresse via la machine à états asynchrone par étapes discrètes. Chaque étape est planifiée sur le TaskScheduler actuel (accessible via TaskScheduler.Current, fournissant par défaut TaskScheduler.Default) ou sur le SynchronizationContext actuel. Si un TaskScheduler est utilisé, chaque étape de la méthode est représentée par une instance de Task qui est transmise à ce TaskScheduler. Par conséquent, un objet Task dans .NET peut représenter deux éléments conceptuels :

  1. Une opération asynchrone qu’il est possible d’attendre. L’exécution de la méthode DelayExecution() ci-dessus est représentée par un objet Task qui peut être attendu.
  2. Dans un bloc de travail synchrone, chaque phase de la méthode DelayExecution() ci-dessus est représentée par un objet Task.

Quand TaskScheduler.Default est utilisé, les continuations sont planifiées directement sur le ThreadPool .NET et ne sont pas enveloppées dans un objet Task. L’enveloppement des continuations dans les instances de Task se produit de manière transparente et les développeurs doivent donc rarement être conscients de ces détails d’implémentation.

Planification de tâches dans Orleans

Chaque activation de grain a sa propre instance de TaskScheduler qui est responsable de l’application du modèle d’exécution monothread des grains. En interne, ce TaskScheduler est implémenté via ActivationTaskScheduler et WorkItemGroup. WorkItemGroup maintient les tâches en file d’attente dans un élément Queue<T>T est un objet Task en interne, et implémente IThreadPoolWorkItem. Pour exécuter chaque objet Task actuellement en file d’attente, WorkItemGroup se planifie lui-même sur le ThreadPool .NET. Quand le ThreadPool .NET appelle la méthode IThreadPoolWorkItem.Execute() du WorkItemGroup, le WorkItemGroup exécute une par une les instances de Task en file d’attente.

Chaque grain a un planificateur qui s’exécute en se planifiant lui-même sur le ThreadPool .NET :

Orleans grains scheduling themselves on the .NET ThreadPool.

Chaque planificateur contient une file d’attente de tâches :

Scheduler queue of scheduled tasks.

Le ThreadPool .NET exécute chaque élément de travail qui lui a été mis en file d’attente. Cela inclut les planificateurs de grain ainsi que d’autres éléments de travail, tels que les éléments de travail planifiés via Task.Run(...) :

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

Remarque

Le planificateur d’un grain peut s’exécuter sur un seul thread à la fois, mais il ne s’exécute pas toujours sur le même thread. Le ThreadPool .NET est libre d’utiliser un thread différent chaque fois que le planificateur du grain est exécuté. Le planificateur du grain est chargé de s’assurer qu’il s’exécute uniquement sur un thread à la fois et c’est la façon dont le modèle d’exécution monothread des grains est implémenté.