Tâches et grains externes
Par conception, toutes les sous-tâches générées à partir de code de grain (par exemple, en utilisant await
, ContinueWith
ou Task.Factory.StartNew
) sont dispatchées sur le même TaskScheduler par activation que la tâches parente et héritent donc du même modèle d’exécution à thread unique que le reste du code de grain. Il s’agit du point principal sur lequel repose l’exécution à thread unique de la concurrence basée sur les tours entre grains.
Dans certains cas, le code de grain peut être amené à « sortir » du modèle de planification des tâches Orleans et à « effectuer quelque chose de spécial », comme faire pointer explicitement un élément Task
vers un autre planificateur de tâches ou .NET ThreadPool. Tel est le cas, par exemple, lorsque le code de grain doit exécuter un appel bloquant à distance synchrone (comme des E/S distantes). L’exécution de cet appel bloquant dans le contexte d’un grain aura pour effet de bloquer le grain, ce qui ne doit jamais se produire. Au lieu de cela, le code de grain peut exécuter ce code bloquant sur le thread du pool de threads et rejoindre (await
) l’achèvement de cette exécution et continuer dans le contexte du grain. Nous pensons que le fait de sortir du planificateur Orleans constitue un scénario d’usage très avancé et rarement nécessaire qui va au-delà des modèles d’utilisation « normaux ».
API basées sur des tâches
await, TaskFactory.StartNew (voir ci-dessous), Task.ContinueWith, Task.WhenAny, Task.WhenAll, Task.Delay respectent toutes le planificateur de tâches actuel. Cela signifie qu’en les utilisant de manière classique, sans transmettre un autre TaskScheduler, elles s’exécuteront dans le contexte du grain.
Task.Run et le délégué
endMethod
de TaskFactory.FromAsyncne respectent pas le planificateur de tâches actuel. Ils utilisent tous deux le planificateurTaskScheduler.Default
, qui est le planificateur de tâches du pool de threads .NET. Par conséquent, le code situé à l’intérieur deTask.Run
et la méthodeendMethod
deTask.Factory.FromAsync
s’exécuteront toujours sur le pool de threads .NET en dehors du modèle d’exécution monothread des grains Orleans. En revanche, tout code venant aprèsawait Task.Run
ouawait Task.Factory.FromAsync
s’exécutera à nouveau sous le planificateur au point où la tâche a été créée, qui est le planificateur du grain.Task.ConfigureAwait avec
false
est une API explicite destinée éviter le planificateur de tâches actuel. Elle amène le code venant après une tâche attendue à s’exécuter sur le planificateur TaskScheduler.Default, qui est le pool de threads .NET, et interrompt par conséquent l’exécution à thread unique du grain.Attention
Vous ne devez en général jamais utiliser
ConfigureAwait(false)
directement dans le code de grain.Les méthodes avec la signature
async void
ne doivent pas être utilisées avec les grains. Elles sont destinées aux gestionnaires d’événements d’interface graphique utilisateur. La méthodeasync void
peut immédiatement bloquer les processus en cours s’ils autorisent l’échappement d’une exception, sans aucun moyen de gérer cette dernière. Cela est également vrai pourList<T>.ForEach(async element => ...)
et toute autre méthode qui accepte un élément Action<T>, car le délégué asynchrone sera converti en déléguéasync void
.
Délégués Task.Factory.StartNew
et async
La recommandation habituelle en matière de planification des tâches dans un programme C# est d’utiliser Task.Run
en faveur de Task.Factory.StartNew
. Une recherche rapide sur Google sur l’utilisation de Task.Factory.StartNew
suggérera qu’il est dangereux et qu’il vaut mieux préférer toujours Task.Run
. Or, si nous voulons nous en tenir au modèle d’exécution à thread unique pour notre grain, nous devons l’utiliser. Alors comment faire ? Le danger lié à l’utilisation de Task.Factory.StartNew()
est qu’il ne prend pas en charge les délégués asynchrones en mode natif. Cela signifie qu’il s’agit probablement d’un bogue : var notIntendedTask = Task.Factory.StartNew(SomeDelegateAsync)
. notIntendedTask
n’est pas une tâche qui aboutit, contrairement à SomeDelegateAsync
. Ainsi, il faut toujours désenvelopper la tâche retournée : var task = Task.Factory.StartNew(SomeDelegateAsync).Unwrap()
.
Exemple de plusieurs tâches et le planificateur des tâches
L’exemple de code suivant montre comment, avec TaskScheduler.Current
, Task.Run
et un planificateur personnalisé spécial, sortir du contexte de grain Orleans et y revenir.
public async Task MyGrainMethod()
{
// Grab the grain's task scheduler
var orleansTS = TaskScheduler.Current;
await Task.Delay(10_000);
// Current task scheduler did not change, the code after await is still running
// in the same task scheduler.
Assert.AreEqual(orleansTS, TaskScheduler.Current);
Task t1 = Task.Run(() =>
{
// This code runs on the thread pool scheduler, not on Orleans task scheduler
Assert.AreNotEqual(orleansTS, TaskScheduler.Current);
Assert.AreEqual(TaskScheduler.Default, TaskScheduler.Current);
});
await t1;
// We are back to the Orleans task scheduler.
// Since await was executed in Orleans task scheduler context, we are now back
// to that context.
Assert.AreEqual(orleansTS, TaskScheduler.Current);
// Example of using Task.Factory.StartNew with a custom scheduler to escape from
// the Orleans scheduler
Task t2 = Task.Factory.StartNew(() =>
{
// This code runs on the MyCustomSchedulerThatIWroteMyself scheduler, not on
// the Orleans task scheduler
Assert.AreNotEqual(orleansTS, TaskScheduler.Current);
Assert.AreEqual(MyCustomSchedulerThatIWroteMyself, TaskScheduler.Current);
},
CancellationToken.None,
TaskCreationOptions.None,
scheduler: MyCustomSchedulerThatIWroteMyself);
await t2;
// We are back to Orleans task scheduler.
Assert.AreEqual(orleansTS, TaskScheduler.Current);
}
Exemple : effectuer un appel de grain à partir d’un code qui s’exécute sur un pool de threads
Un autre scénario est un exemple de code grain qui doit « sortir » du modèle de planification des tâches du grain et s’exécuter sur un pool de threads (ou en dehors d’un contexte de grain), mais qui doit encore appeler un autre grain. Les appels de grain peuvent être effectués en dehors de contextes de grain sans étapes supplémentaires.
Le code ci-dessous montre comment appeler un grain à partir d’un code qui s’exécute à l’intérieur d’un grain, mais en dehors d’un contexte de grain.
public async Task MyGrainMethod()
{
// Grab the Orleans task scheduler
var orleansTS = TaskScheduler.Current;
var fooGrain = this.GrainFactory.GetGrain<IFooGrain>(0);
Task<int> t1 = Task.Run(async () =>
{
// This code runs on the thread pool scheduler,
// not on Orleans task scheduler
Assert.AreNotEqual(orleansTS, TaskScheduler.Current);
int res = await fooGrain.MakeGrainCall();
// This code continues on the thread pool scheduler,
// not on the Orleans task scheduler
Assert.AreNotEqual(orleansTS, TaskScheduler.Current);
return res;
});
int result = await t1;
// We are back to the Orleans task scheduler.
// Since await was executed in the Orleans task scheduler context,
// we are now back to that context.
Assert.AreEqual(orleansTS, TaskScheduler.Current);
}
Utiliser des bibliothèques
Certaines bibliothèques externes utilisées par votre code peuvent utiliser ConfigureAwait(false)
en interne. Utiliser ConfigureAwait(false)
lors de l’implémentation de bibliothèques à usage général est une bonne pratique dans .NET. Cela ne pose pas de problème dans Orleans. Tant que le code contenu dans le grain qui appelle la méthode de bibliothèque attend l’appel de bibliothèque avec un await
normal, le code de grain est correct. Le résultat sera exactement comme attendu : le code de la bibliothèque exécutera des continuations sur le planificateur par défaut (la valeur retournée par TaskScheduler.Default
, qui ne garantit pas que les continuations s’exécuteront sur un thread ThreadPool, car les continuations sont souvent incluses dans le thread précédent), tandis que le code de grain s’exécutera sur le planificateur du grain.
Une autre question souvent posée est celle qui vise à déterminer s’il est nécessaire d’exécuter les appels de bibliothèque avec Task.Run
, autrement dit, s’il est nécessaire de décharger explicitement le code de bibliothèque dans ThreadPool
(pour que le code de fragment de temps fasse Task.Run(() => myLibrary.FooAsync())
). La réponse est négative. Il n’est pas nécessaire de décharger du code dans ThreadPool
sauf dans le cas où le code de bibliothèque effectue des appels synchrones bloquants. En règle générale, une bibliothèque asynchrone .NET bien écrite et correcte (méthodes qui retournent Task
et dont le nom se compose d’un suffixe Async
) n’effectue pas d’appels bloquants. Par conséquent, il n’est pas nécessaire de décharger quoi que ce soit dans ThreadPool
à moins que vous soupçonniez la bibliothèque asynchrone d’être sujette à des bogues ou si vous utilisez délibérément une bibliothèque bloquante synchrone.
Blocages
Étant donné que les grains s’exécutent en mode monothread, il est possible de bloquer un grain par un blocage synchrone dont le déblocage nécessiterait plusieurs threads. Cela signifie que le code qui appelle l’une des méthodes et propriétés suivantes peut bloquer un grain si les tâches fournies n’ont pas encore été effectuées au moment où la méthode ou la propriété est appelée :
Task.Wait()
Task.Result
Task.WaitAny(...)
Task.WaitAll(...)
task.GetAwaiter().GetResult()
Ces méthodes doivent être évitées dans n’importe quel service à forte concurrence, car elles peuvent aboutir à de mauvaises performances et à une instabilité en affamant le ThreadPool
.NET par le blocage de threads qui pourraient effectuer un travail utile et obliger le ThreadPool
.NET à injecter des threads supplémentaires afin qu’ils puissent être terminés. Pendant l’exécution de code de grain, comme nous l’avons vu ci-dessus, ces méthodes peuvent occasionner un blocage du grain et doivent par conséquent être également évitées dans le code de grain.
Si un travail sync-over-async ne peut pas être évité, il est préférable de déplacer ce travail vers un planificateur distinct. Le moyen le plus simple d’y parvenir est d’utiliser await Task.Run(() => task.Wait())
par exemple. Notez qu’il est fortement recommandé d’éviter tout travail sync-over-async, car comme nous l’avons vu ci-dessus, la scalabilité et le niveau de performance de votre application en pâtissent.
Résumé de l’utilisation des Task dans Orleans
Quelle tâche essayez-vous d’effectuer ? | Comment procéder |
---|---|
Exécutez le travail en arrière-plan sur des threads di pool de threads .NET. Aucun code de grain ou appel de grain n’est autorisé. | Task.Run |
Exécuter une tâche worker asynchrone à partir de code de grain avec les garanties de la concurrence basée sur les tours de Orleans (cf. plus haut) | Task.Factory.StartNew(WorkerAsync).Unwrap() (Unwrap) |
Exécuter une tâche worker synchrone à partir de code de grain avec les garanties de la concurrence basée sur les tours de Orleans | Task.Factory.StartNew(WorkerSync) |
Délais d’expiration pour l’exécution d’éléments de travail | Task.Delay + Task.WhenAny |
Appeler une méthode de bibliothèque asynchrone | await pour l’appel de bibliothèque |
Utiliser async /await |
Le modèle de programmation normal Task-Async de .NET. Pris en charge et recommandé |
ConfigureAwait(false) |
À ne pas utiliser à l’intérieur du code de grain. Autorisé uniquement à l’intérieur des bibliothèques. |