Guide

Come Creare un Servizio Windows in .Net

Questo articolo affronta la creazione di un servizio windows .net con tre approcci diversi. Il più comune è quello basato su un timer (timer-based service), che semplicemente invoca il thread in background periodicamente…

Esistono anche le altre due alternative, a single thread e multi thread. Mentre l’approccio a timer è il più semplice, quello a single thread e sopratutto quello multi thread offrono alcuni vantaggi aggiuntivi.

La scrittura di un servizio .net è molto semplice. Bisogna semplicemente creare un progetto usando il Windows Service template. Poi bisogna inizializzare il servizio nell’evento OnStart e gestire la terminazione nell’evento OnStop aggiunto dal modello di Visual Studio.

protected override void OnStart(string[] args)
{
// TODO: Add code here to start your service.
}

protected override void OnStop()
{
// TODO: Add code here to perform any tear-down necessary
// to stop your service.
}
La soluzione con il Timer

Naturalmente, non è molto difficile far richiamre le funzioni che effettuano il lavoro vero e proprio, bisogna solo scrivere poche righe di codice per gestire il timer che attiva le chiamate periodicamente.
L’approccio a timer è il metodo più comune ed è probabilmente il più semplice da scrivere e capire. Devi creare un timer sull’evento OnStart ed agganciare le funzioni che effettuano il lavoro al timer.
Ecco l’esempio della soluzione a Timer:

// declare class-level variable for the timer
private Timer serviceTimer;

protected override void OnStart(string[] args)
{
TimerCallback timerDelegate =
new TimerCallback(DoWork);

// create timer and attach our method delegate to it
serviceTimer =
new Timer(timerDelegate, null, 1000, _interval);
}
DoWork è il metodo che contiene il codice per fare quello che vuoi periodicamente, quando scatta il timer.
Il prossimo listato mostra l’implementazione del metodo DoWork che effettua semplicemente la scrittura nell’EventLog.

private void DoWork(object state)
{
if (_workStartTime != DateTime.MinValue)
{
// probably check how much time has elapsed since work
// started previously and log any warning
EventLog.WriteEntry(“Warning! Worker busy since ” +
_workStartTime.ToLongTimeString(),
System.Diagnostics.EventLogEntryType.Warning);
}
else
{
// set work start time
_workStartTime = DateTime.Now;

// Do some work
// Note: Exception handling is very important here
// if you don’t, the error will vanish along with your worker thread
try
{
EventLog.WriteEntry (“Timer Service Tick :” + DateTime.Now.ToString());
}
catch (System.Exception ex)
{
// replace this with some robust logging technique
EventLog.WriteEntry(“Error! ” + ex.Message,
System.Diagnostics.EventLogEntryType.Error);
}

// reset work start time
_workStartTime = DateTime.MinValue;
}
}

Per ogni evento del timer si controlla se il lavoro che si sta eseguendo deriva dal precedente evento. Questo viene testato settando la variabile “_workStartTime” a DateTime.Now quando la funzione DoWork parte. La variabile è resettata a DateTime.MinValue quando tutto il lavoro da eseguire è completato.
Se la funzione sta ancora girando ed non ha finito registriamo un warning.

Si noti la gestione degli errori nella funzione DoWork. Se non gestite le eccezzioni e ne capitasse una, non riuscireste mai a sapere che errore è accaduto ed il vostro worker thread si limiterebbe a morire. Il servizio continuerebbe a funzionare normalmente senza sapere che il thread è terminato.

Con poche righe di codice abbiamo realizzato quello che volevamo. Ma ci sono altri modi per fare la stessa cosa ma in modo più elegante, vediamo come.

Alternativa 1: Usiamo un Thread Separato

La prima volta che ho scritto un servizio, mi son chiesto se potevo fare qualcosa di simile al listato sotto piuttosto che prendermi la briga di aggiungere un timer.

Il servizio instoppabile

protected override void OnStart(string[] args)
{
while (true)
{
// do some work

// idle
Thread.Sleep(0, interval, 0)
}
}
Ma subito mi son reso conto che cosi facendo il servizio sembrava rimaner appeso. Quando facevo partire il servizio non ottienevo una risposta da windows fintantoche il servizio non si bloccava nell’evento OnStart e il sistema operativo segnalava un errore. Un altro problema stava nel fatto che cosi facendo non si poteva dire al servizio di arrestarsi, perchè girava sempre nell’evento OnStart!

Per risolvere allora possiamo fare qualcosa di simile (noi dobbiamo invocare il metodo worker di lavoro). Useremo un thread separato. Iniziamo dichiarando alcune variabili di classe.

// This is a flag to indicate the service status
private bool serviceStarted = false;

// the thread that will do the work
Thread workerThread;

Il listato sotto mostra l’esempio con il single thread nell’evento OnStart

protected override void OnStart(string[] args)
{
// Create worker thread; this will invoke the WorkerFunction
// when we start it.
// Since we use a separate worker thread, the main service
// thread will return quickly, telling Windows that service has started
ThreadStart st = new ThreadStart(WorkerFunction);
workerThread = new Thread(st);

// set flag to indicate worker thread is active
serviceStarted = true;

// start the thread
workerThread.Start();
}
Il codice precedente istanzia un thread separato e vi attacca la nostra funzione “WorkerFunction”, nella quale metteremo il codice per eseguire i nostri task. Poi fa partire il thread e fa terminare l’evento OnStart, cosi che Windows non pensi che il servizio sia rimasto appeso.

L’implementazione della funzione WorkerFunction con l’implementazione single-thread

///

/// This function will do all the work
/// Once it is done with its tasks, it will be suspended for some time;
/// it will continue to repeat this until the service is stopped
///

private void WorkerFunction()
{
// start an endless loop; loop will abort only when “serviceStarted”
// flag = false
while (serviceStarted)
{
// do something
// exception handling omitted here for simplicity
EventLog.WriteEntry(“Service working”,
System.Diagnostics.EventLogEntryType.Information);

// yield
if (serviceStarted)
{
Thread.Sleep(new TimeSpan(0, interval, 0);
}
}

// time to end the thread
Thread.CurrentThread.Abort();
}
Questa funzione girà in un loop senza fine fino a quando la variabile “serviceStarted” non diventa false. Il parametro viene settato nell’evento OnStop come potete vedere nel listato sotto.

Implementazione dell’evento OnStop con l’implementazione single-thread

protected override void OnStop()
{
// flag to tell the worker process to stop
serviceStarted = false;

// give it a little time to finish any pending work
workerThread.Join(new TimeSpan(0,2,0));
}
Notate che abbiamo lasciato un tempo ragionevole al thread che esegue il lavoro (2 minuti nel listato sopra) affinchè riesca a completare il suo task. Ma se non ci riuscisse in quel tempo, la clausola “workerThread.Join” lo fa terminare in qualunque caso.

Alternativa 2: Usiamo più Threads (Multiple Threads)

Questa tecnica è molto simile alla precedente con il single-thread, ma il concetto viene esteso usando più threads. Questo pattern può semplicemente rimpiazzare l’alternativa a single-thread, usando soltanto un thread nell’array. Ora vediamo il codice. Per prima cosa dichiariamo le variabili di classe.

Variabili di classe nell’approccio multithread

// array of worker threads
Thread[] workerThreads;

// the objects that do the actual work
Worker[] arrWorkers;

// number of threads; typically specified in config file
int numberOfThreads = 2;
Vediamo l’evento OnStart

protected override void OnStart(string[] args)
{
arrWorkers = new Worker[numberOfThreads];
workerThreads = new Thread[numberOfThreads];
for (int i =0; i < numberOfThreads; i++)
{
// create an object
arrWorkers[i] = new Worker(i+1, EventLog);

// set properties on the object
arrWorkers[i].ServiceStarted = true;

// create a thread and attach to the object
ThreadStart st = new ThreadStart(arrWorkers[i].ExecuteTask);
workerThreads[i] = new Thread(st);
}

// start the threads
for (int i = 0; i < numberOfThreads; i++) { workerThreads[i].Start(); } } Per prima cosa creiamo un array di Worker objects della dimensione che ci necessita. Il Worker è una classe separata che contiene un metodo chiamato ExecuteTask che fa tutto il lavoro. Poi andiamo a dichiarare un array di threads e ci attacchiamo un Worker object ad ogni thread, specificando ExecuteTask come metodo da chiamare. Infine, lanciamo tutti i threads. Dopo di che, il metodo OnStart termina e il controllo ritorna a Windows. Il metodo “ExecuteTask” è simile ai metodi worker dei precedenti esempi. Il metodo ExecuteTask (Worker) nell’approccio multithread public void ExecuteTask() { DateTime lastRunTime = DateTime.UtcNow; while (serviceStarted) { // check the current time against the last run plus interval if ( ((TimeSpan) (DateTime.UtcNow.Subtract(lastRunTime))).TotalSeconds >= _interval)
{
// if time to do something, do so
// exception handling omitted here for simplicity
_serviceEventLog.WriteEntry(
“Multithreaded Service working; id = ” + this._id.ToString(),
EventLogEntryType.Information);

// set new run time
lastRunTime = DateTime.UtcNow;
}

// yield
if (serviceStarted)
{
Thread.Sleep(new TimeSpan(0,0,15));
}
}

Thread.CurrentThread.Abort();
}
Notate che usiamo un piccolo intervallo per il metodo Thread.Sleep (15 secondi) per aumentare la reattività quando il servizio viene arrestato. Se non avessimo fatto cosi il servizio sarebbe rimasto bloccato fino alla terminazione del periodo _interval (oppure sarebbe semplicemente andato in time out se l’intervallo fosse stato troppo lungo).

L’evento OnStop è molto simile a quello trattato nell’approccio single-thread, ad eccezzione del fatto che qui dobbiamo notificare a tutti i thread lo stop.

Metodo OnStop per l’approccio multithread

protected override void OnStop()
{
for (int i = 0; i < numberOfThreads; i++)
{
// set flag to stop worker thread
arrWorkers[i].ServiceStarted = false;

// give it a little time to finish any pending work
workerThreads[i].Join(new TimeSpan(0,2,0));
}
}

Compariamo i vari approcci

Oltre che per l’implementazione i tre metodi differisco in alcuni aspetti:

Performance Non ci sembra essere molta differenza anche se l’approccio col timer richiede un paio di thread supplementari per il timer.
Implementazione (codifica) L’approccio timer è il più semplice, quello a single-thread è leggermente più complesso e l’approccio con multithread richiede più codice.
Possibilità di eseguire più thread Ciò è disponibile nell’approccio multithread. Il numero di thread può essere specificato nel file di config, cosi chè è molto semplice aumentare o diminuire i worker thread. E’ inoltre possibile realizzare più thread anche nell’approccio timer, ma con codice aggiuntivo.
Possibilità di settaggio dei ritardi per ogni thread Con l’approccio multithread è possibile specificare il ritardo per ogni thread nel file di configurazione. L’approccio con il timer non è cosi flessibile; il timer interviene ad orari specifici, e per ottere la stessa cosa bisogna fare qualche accrocchio.
Il ritardo di un thread disturba l’esecuzione degli altri thread Questo non può accadere nell’approccio multithread. Ogni thread è in esecuzione ed ignora gli altri thread. Invece nell’approccio con il timer, se intervalliamo più worker thread, quello con tempo di esecuzione più lungo determina quando l’esecuzione successiva sarà possibile.

E ‘anche possibile generare più servizi all’interno dello stesso processo del servizio principale. Il listato sotto mostra il metodo Main() che il modello dei servizi .net genera.

static void Main()
{
System.ServiceProcess.ServiceBase[] ServicesToRun;

// More than one user service may run within the same process. To add
// another service to this process, change the following line to
// create a second service object. For example,
//
// ServicesToRun = new System.ServiceProcess.ServiceBase[]
// {new Service1(), new MySecondUserService()};
//
ServicesToRun = new System.ServiceProcess.ServiceBase[]
{ new Service1() };

System.ServiceProcess.ServiceBase.Run(ServicesToRun);
}
Non ho parlato di questa opzione in questo articolo perché abbiamo considerato la possibilità di creare più thread in background in un determinato servizio, piuttosto che avere più servizi in esecuzione nello stesso processo.

Conclusioni

Questo articolo ha spiegato nel dettaglio come creare un servizio in .net con tre differenti approcci. Metre l’approccio col timer è il più semplice, quello multithread è molto flessibile e può lavorare con uno o più thread. Quest’ultimo approccio permette anche di gestire diversi ritardi per ogni thread. Qualsiasi ritardo di esecuzione nel metodo worker di un thread non interferirà con l’esecuzione degli altri thread.