Vue d’ensemble de la planification
Dans Orleans, il existe deux formes de planification qui se rapportent aux grains :
- 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.
- 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 :
- Le premier appel à
_logger.LogInformation(...)
et l’appel àTask.Delay(1_000)
. - 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 :
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 :
- 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 objetTask
qui peut être attendu. - Dans un bloc de travail synchrone, chaque phase de la méthode
DelayExecution()
ci-dessus est représentée par un objetTask
.
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> où 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 :
Chaque planificateur contient une file d’attente de tâches :
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(...)
:
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é.