Accueil

Boucle sans fin

by Jean-Camille Mercier 27. juin 2016 17:14

J'ai eu le cas récemment de relire un code C# utilisant une BlockingCollection<T> qui est un peu particulière car elle permet de dépiler les items de la collection au fur et à mesure qu'on en ajoute (donc threadsafe) et en plus, la petite maline bloque le thread producteur si la collection est pleine ! Bon sur le papier ça peut être cool parce qu'en procédant ainsi, la pile ne grossit pas trop et on grossit pas en mémoire ... mais bon ... le thread est bloqué quoi !!! En fonction du sénario, ça peut justement être très BLOQUANT et engendrer un max de fragmentation ...

Donc, comme souvent, back to basic : j'ai mis en place un bonne vieille stack threadsafe : ConcurrentQueue<T> avec un thread et une boucle sans fin ! C'est l'occasion de revenir sur cette conception simple mais pourtant bigrement efficace qu'est le while(true). Cette boucle est souvent associée à problème, pourtant, bien maitrisée, elle fait des miracles. Il n'y d'ailleurs que 3 choses à bien vérifier : 

1 - La gestion du thread

Qui dit thread, dit forcement clôture bien propre du thread, pour se faire, vous devez utiliser un objet tout simple, le "CancellationTokenSource" qu'il faut passer à la création du thread et qui, lorsque vous appelez sa méthode "Cancel()", va dire au thread de s'arreter gentiment parce qu'il est bien élevé codé. 

Ensuite, quel objet thread utiliser ? Il y a le classique System.Threading.Thread connu depuis la nuit des temps, qui porte bien son nom parce qu'il crée simplement un thread au sens OS du terme, c'est l'objet de base, le truc avec lequel vous serez jamais déçu ! Ensuite on a le ThreadPool, qui est un peu plus haut niveau et qui va gérer un ensemble de thread applicatif pour pas polluer l'OS, si vous faite beaucoup de création / destruction de petits thread dans votre appli, c'est l'objet idéal. Et puis ya le petit nouveau, le System.Threading.Tasks.Task qui est la nouvelle manière de faire avec les mots clefs async / await, c'est le thread next-gen, celui qui résoud tous vos problèmes et qui fait le café ...

2 - La gestion des exception

Mais revenons à notre boucle sans fin, qui a aussi besoin de bien savoir gérer ses exceptions, alors biensur, vous mettez un try/catch dans la boucle pour éviter les bétises, mais en plus, parce qu'il arrive toujours des trucs auxquels on a pas pensé, on ajoute un "ContinueWith()" à notre task pour la faire repartir ni vu ni connu !

3 - Laissez-la respirer !!

Et c'est bien souvent là que le bât blesse, si votre boucle sans fin tourne tout le temps, votre processeur est à 100% et vous pas content ! Donc il faut absolument mettre un petit Thread.Sleep() dans votre boucle pour laisser les autres processus prendre la main et continuer leurs jobs. Votre boucle sans fin aura ainsi le droit de parler comme les autres et non pas tout le temps. La valeur que vous aller mettre dans ce sleep est assez déterminante, 500 millisecondes c'est énorme mais pour un thread background c'est correcte, il est possible de descendre à 10 voir à 2 pour un traitement UI ou real-time. Il faut faire des tests et doser ...

Finalement, c'était pas si compliqué hein ? Aller le code type : 

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace ConsoleApplication1
{
    public class MonManager
    {
        private readonly CancellationTokenSource _cancel;
        private readonly Task _worker;

        public MonManager()
        {
            _cancel = new CancellationTokenSource();
            _worker = new Task(DoWork, _cancel.Token, TaskCreationOptions.LongRunning);
        }

        private void DoWork()
        {
            do
            {
                try
                {
                    // Le code de la boucle sans fin
                    // ...

                    // Avec le sleep pour laisser respirer
                    Thread.Sleep(500);
                }
                catch (ExceptionMetier ex)
                {
                    // Gestion des exceptions métiers
                    throw;
                }
            }
            while (!_cancel.IsCancellationRequested);
        }

        public void Start()
        {
            _worker.Start();
            _worker.ContinueWith(WorkerException, TaskContinuationOptions.OnlyOnFaulted);
        }

        public void Stop()
        {
            if (!_cancel.IsCancellationRequested)
                _cancel.Cancel();
        }

        private void WorkerException(Task task)
        {
            // Gestion des exceptions techniques
            if (!_cancel.IsCancellationRequested)
                this.Start();
        }
    }
}

 

 Avec un code aussi simple, pourquoi se priver ?