Quelle est la bonne façon d’utiliser async / wait dans une méthode récursive?

Quelle est la bonne façon d’utiliser async / wait dans une méthode récursive? Voici ma méthode:

public ssortingng ProcessStream(ssortingng streamPosition) { var stream = GetStream(streamPosition); if (stream.Items.count == 0) return stream.NextPosition; foreach(var item in stream.Items) { ProcessItem(item); } return ProcessStream(stream.NextPosition) } 

Et voici la méthode avec async / wait:

 public async Task ProcessStream(ssortingngstreamPosition) { var stream = GetStream(streamPosition); if (stream.Items.count == 0) return stream.NextPosition; foreach(var item in stream.Items) { await ProcessItem(item); //ProcessItem() is now an async method } return await ProcessStream(stream.NextPosition); } 

Bien que je dois dire d’emblée que l’intention de la méthode n’est pas tout à fait claire pour moi, la réimplémenter avec une simple boucle est assez sortingviale:

 public async Task ProcessStream(ssortingng streamPosition) { while (true) { var stream = GetStream(streamPosition); if (stream.Items.Count == 0) return stream.NextPosition; foreach (var item in stream.Items) { await ProcessItem(item); //ProcessItem() is now an async method } streamPosition = stream.NextPosition; } } 

La récursivité n’est pas conviviale pour la stack et si vous avez la possibilité d’utiliser une boucle, c’est vraiment quelque chose de vraiment intéressant dans les scénarios synchrones simples (où une récursivité mal contrôlée mène finalement à StackOverflowException s), ainsi qu’à des scénarios asynchrones, où, Soyons honnêtes, je ne sais même pas ce qui se passerait si vous poussiez les choses trop loin (mon VS Test Explorer se bloque lorsque j’essaie de reproduire des scénarios de débordement de stack connus avec des méthodes async ).

Des réponses telles que Récurrence et les mots-clés StackOverflowException / async suggèrent que StackOverflowException moins de problèmes avec async raison de la façon dont fonctionne la machine d’état async/await , mais ce n’est pas quelque chose que j’ai exploré autant que j’ai tendance à éviter la récursion autant que possible.

Lorsque j’ajoute du code pour rendre votre exemple plus concret, je trouve deux manières possibles pour que la récursivité tourne mal. Les deux supposent que vos données sont assez volumineuses et nécessitent le déclenchement de conditions spécifiques.

  1. Si ProcessItem(ssortingng) retourne une Task qui se termine avant d’être ProcessItem(ssortingng) ou, je suppose, qu’elle se termine avant que l’ await n’ait fini de tourner), la suite sera exécutée de manière synchrone. Dans mon code ci-dessous, j’ai simulé cela en ProcessItem(ssortingng) renvoyer Task.CompletedTask . Lorsque je fais cela, le programme se termine très rapidement avec une StackOverflowException . Cela est dû au fait que la TPL de .net « libère Zalgo » en exécutant de manière opportuniste les continuations de manière synchrone, sans tenir compte de la quantité d’espace disponible dans la stack actuelle. Cela signifie que cela va exacerber le problème potentiel d’espace de stack que vous avez déjà en utilisant un algorithme récursif. Pour le voir, await Task.Yield(); dans mon exemple de code ci-dessous.
  2. Si vous utilisez une technique pour empêcher la TPL de continuer de manière asynchrone (j’utilise Task.Yield() ci-dessous), le programme finira par manquer de mémoire et mourra avec une OutOfMemoryException . Si je comprends bien, cela ne se produirait pas si return await était capable d’émuler l’optimisation de l’appel final. J’imagine que ce qui se passe ici, c’est que chaque appel génère quelque chose comme une Task comptabilité Task et continue de les générer même s’ils peuvent être fusionnés. Pour reproduire cette erreur avec l’exemple ci-dessous, assurez-vous que vous exécutez le programme en 32 bits, désactivez l’appel Console.WriteLine() (car les consoles sont très lentes) et assurez-vous que await Task.Yield() est en await Task.Yield() .
 using System; using System.Collections.Generic; using System.Threading.Tasks; // Be sure to run this 32-bit to avoid making your system unstable. class StreamProcessor { Stream GetStream(ssortingng streamPosition) { var parsedStreamPosition = Convert.ToInt32(streamPosition); return new Stream( // Terminate after we reach 0. parsedStreamPosition > 0 ? new[] { streamPosition, } : new ssortingng[] { }, Convert.ToSsortingng(parsedStreamPosition - 1)); } Task ProcessItem(ssortingng item) { // Comment out this next line to make things go faster. Console.WriteLine(item); // Simulate the Task represented by ProcessItem finishing in // time to make the await continue synchronously. return Task.CompletedTask; } public async Task ProcessStream(ssortingng streamPosition) { var stream = GetStream(streamPosition); if (stream.Items.Count == 0) return stream.NextPosition; foreach (var item in stream.Items) { await ProcessItem(item); //ProcessItem() is now an async method } // Without this yield (which prevents inline synchronous // continuations which quickly eat up the stack), // you get a StackOverflowException fairly quickly. // With it, you get an OutOfMemoryException eventually—I bet // that “return await” isn't able to tail-call properly at the Task // level or that TPL is incapable of collapsing a chain of Tasks // which are all set to resolve to the value that other tasks // resolve to? await Task.Yield(); return await ProcessStream(stream.NextPosition); } } class Program { static int Main(ssortingng[] args) => new Program().Run(args).Result; async Task Run(ssortingng[] args) { await new StreamProcessor().ProcessStream( Convert.ToSsortingng(int.MaxValue)); return 0; } } class Stream { public IList Items { get; } public ssortingng NextPosition { get; } public Stream( IList items, ssortingng nextPosition) { Items = items; NextPosition = nextPosition; } } 

Donc, je suppose que mes deux recommandations sont les suivantes:

  1. Utilisez Task.Yield() si vous n’êtes pas certain que la croissance de la stack de la récursivité sera interrompue par autre chose.
  2. Comme cela a déjà été suggéré , évitez la récursivité si cela n’a aucun sens pour votre problème. Et même si cela crée un algorithme pur, évitez-le si la taille de votre problème est illimitée.