web-dev-qa-db-fra.com

Service Windows OnStop en attente du traitement terminé

En fait, je développe un service Windows sous VS 2012/.NET 4.5.

Le service suit le schéma de l'extrait de code ci-dessous: 

  • Utiliser une minuterie
  • Exécute une opération souhaitée toutes les deux minutes. 
  • Le processus prend environ 10 minutes.
  • J'utilise un seul thread dans le service

Ce qui m'inquiète, c'est que si quelqu'un arrête le service via la console de gestion, c'est peut-être juste pendant le processus que le service effectue.

J'ai lu des articles sur l'arrêt du service Windows avec stop requête, mais je suis un peu perdu. Parfois, des WorkerThreads sont créés, parfois des ManualResetEvents sont créés, mais je ne pouvais pas encore saisir la meilleure voie à suivre pour mon service Windows.

Je dois attendre que le traitement se termine correctement avec la méthode onStop avant d'arrêter le service Windows.

Quelle est la meilleure voie à suivre, en tenant compte également de l'extrait de code ci-dessous?

Merci a tous!

namespace ImportationCV
{
    public partial class ImportationCV : ServiceBase
    {
        private System.Timers.Timer _oTimer;       

        public ImportationCV()
        {
            InitializeComponent();

            if (!EventLog.SourceExists(DAL.Utilities.Constants.LOG_JOURNAL))
            {
                EventLog.CreateEventSource(DAL.Utilities.Constants.LOG_JOURNAL,     DAL.Utilities.Constants.SOURCE_JOURNAL);
            }

            EventLog.Source = DAL.Utilities.Constants.SOURCE_JOURNAL;
            EventLog.Log = DAL.Utilities.Constants.LOG_JOURNAL;
        }

        protected override void OnStart(string[] args)
        {            
            int intDelai = Properties.Settings.Default.WatchDelay * 1000;

            _oTimer = new System.Timers.Timer(intDelai);
            _oTimer.Elapsed += new ElapsedEventHandler(this.Execute);

            _oTimer.Start();           

            EventLog.WriteEntry(DAL.Utilities.Constants.LOG_JOURNAL, "Service " + DAL.Utilities.Constants.SERVICE_TITLE + " started at " + DateTime.Now.ToString("HH:mm:ss"), EventLogEntryType.Information);
        }

        protected override void OnStop()
        {

            if (_oTimer != null && _oTimer.Enabled)
            {
                _oTimer.Stop();
                _oTimer.Dispose();
            }

            EventLog.WriteEntry(DAL.Utilities.Constants.LOG_JOURNAL, "Service " + DAL.Utilities.Constants.SERVICE_TITLE + " stopped at " + DateTime.Now.ToString("HH:mm:ss"), EventLogEntryType.Information);
        }

        private void Execute(object source, ElapsedEventArgs e)
        {
            _oTimer.Stop();

            try
            {                
                //Process


            }
            catch (Exception ex)
            {
                EventLog.WriteEntry(DAL.Utilities.Constants.LOG_JOURNAL, (ex.StackTrace + ("\r\n" + ex.Message)), EventLogEntryType.Error);
            }

            _oTimer.Start();
        }
    }
}
13
crisjax

En guise de test, j'ai appelé System.Threading.Thread.Sleep(500000) dans le rappel OnStop() de mon service Windows. J'ai commencé le service puis je l'ai arrêté. J'ai eu la fenêtre avec la barre de progression indiquant que le gestionnaire de contrôle de service (SCM) essayait d'arrêter le service. Après environ 2 minutes, j'ai reçu cette réponse du SCM:

enter image description here

Après avoir fermé cette fenêtre, le statut de mon service dans le GDS est passé à Arrêt et j'ai constaté que le service continuait de s'exécuter dans le Gestionnaire des tâches. Une fois le sommeil écoulé (près de 6 minutes plus tard), le processus s'est arrêté. L'actualisation de la fenêtre SCM a montré que le service n'était plus en cours d'exécution.

Je retiens deux choses de cela. Tout d’abord, OnStop() devrait réellement essayer d’interrompre le service rapidement tout en jouant à Nice avec le système. Deuxièmement, en fonction de la structure de votre méthode OnStop(), vous pouvez forcer le service à ignorer une demande préemptive afin de s’arrêter, au lieu de vous arrêter lorsque vous le dites. Ce n'est pas recommandé, mais il semble que vous l'ayez fait pourriez.

En ce qui concerne votre situation particulière, vous devez comprendre que l'événement System.Timers.Timer.Elapsedest déclenché sur un thread ThreadPool . Par définition, il s'agit d'un thread d'arrière-plan , ce qui signifie que l'application ne sera pas exécutée. Lorsque le service est invité à s'arrêter, le système arrête tous les threads d'arrière-plan, puis quitte le processus. Donc, votre souci de maintenir le traitement en cours jusqu'à ce qu'il soit terminé bien que le SCM vous ait dit d'arrêter ne peut pas se produire de la manière dont vous avez structuré les choses. Pour ce faire, vous devez créer un objet System.Threading.Thread formel, le définir en tant que thread de premier plan, puis utiliser le minuteur pour déclencher l’exécution de ce thread (au lieu d’être effectué dans le rappel Elapsed).

Tout cela étant dit, je pense toujours que vous voudrez jouer avec le système, ce qui signifie l’arrêt du service en temps voulu quand on le lui demande. Que se passe-t-il si, par exemple, vous devez redémarrer la machine? Je ne l'ai pas testé, mais si vous obligez votre service à continuer de s'exécuter jusqu'à la fin du traitement, le système risque en effet d'attendre la fin du processus avant de redémarrer réellement. Ce n'est pas ce que je voudrais de mon service.

Donc, je suggérerais une des deux choses. La première option serait de diviser le traitement en plusieurs parties pouvant être effectuées individuellement. À la fin de chaque bloc, vérifiez si le service s’arrête. Si c'est le cas, quittez le fil avec grâce. Si cela ne peut pas être fait, j'introduirais dans votre traitement quelque chose qui ressemble à des transactions. Supposons que vous deviez interagir avec un ensemble de tables de base de données et que l'interruption du flux une fois démarré devient problématique, car la base de données risque de rester dans un mauvais état. Si le système de base de données autorise les transactions, cela devient relativement facile. Sinon, effectuez tous les traitements en mémoire et validez les modifications à la dernière seconde. De cette façon, vous ne bloquez que la fermeture pendant que les modifications sont validées, par opposition au blocage pour toute la durée. Et pour ce que cela vaut, je préfère utiliser ManualResetEvent pour communiquer les commandes d'arrêt aux threads.

Pour éviter de perdre plus de temps, je vais couper ça ici. HTH.

MODIFIER:

Ceci n’a pas lieu, donc je ne vérifierai pas son exactitude. Je corrigerai tout problème que vous (ou d'autres) pourrez trouver.

Définissez deux objets ManualResetEvent, un pour la notification d'arrêt et un pour le traitement de la notification, et l'objet Thread. Changez le callback OnStart() en ceci:

using System.Threading;
using Timer = System.Timers.Timer; // both Threading and Timers have a timer class

ManualResetEvent _shutdownEvent = new ManualResetEvent(false);
ManualResetEvent _processEvent  = new ManualResetEvent(false);
Thread _thread;
Timer _oTimer;

protected override void OnStart(string[] args)
{
    // Create the formal, foreground thread.
    _thread = new Thread(Execute);
    _thread.IsBackground = false;  // set to foreground thread
    _thread.Start();

    // Start the timer.  Notice the lambda expression for setting the
    // process event when the timer elapses.
    int intDelai = Properties.Settings.Default.WatchDelay * 1000;
    _oTimer = new Timer(intDelai);
    _oTimer.AutoReset = false;
    _oTimer.Elapsed += (sender, e) => _processEvent.Set();
    _oTimer.Start();
}

Changez votre callback Execute() en quelque chose comme ceci:

private void Execute()
{
    var handles = new WaitHandle[] { _shutdownEvent, _processEvent };

    while (true)
    {
        switch (WaitHandle.WaitAny(handles))
        {
            case 0:  // Shutdown Event
                return; // end the thread
            case 1:  // Process Event
                Process();
                _processEvent.Reset();  // reset for next time
                _oTimer.Start();        // trigger timer again
                break;
        }
    }
}

Créez la méthode Process() comme ceci:

private void Process()
{
    try
    {
        // Do your processing here.  If this takes a long time, you might
        // want to periodically check the shutdown event to see if you need
        // exit early.
    }
    catch (Exception ex)
    {
        // Do your logging here...

        // You *could * also shutdown the thread here, but this will not
        // stop the service.
        _shutdownEvent.Set();
    }
}

Enfin, dans le callback OnStop(), déclenchez l’arrêt du thread:

protected override void OnStop()
{
    _oTimer.Stop();  // no harm in calling it
    _oTimer.Dispose();

    _shutdownEvent.Set();  // trigger the thread to stop
    _thread.Join();        // wait for thread to stop
}
19
Matt Davis

@ Matt - Merci pour le code génial, très utile. J'ai trouvé que cela fonctionnait encore mieux si j'ajoutais un autre test sur _shutdownEvent:

case 1:  // Process Event
            Process();
            if(_shutdownEvent.WaitOne(0)) break; // don't loop again if a shutdown is needed
...
0
Allister